cmux/cmuxTests/WorkspaceUnitTests.swift
2026-03-23 19:16:39 -07:00

2144 lines
84 KiB
Swift

import XCTest
import AppKit
import SwiftUI
import UniformTypeIdentifiers
import WebKit
import ObjectiveC.runtime
import Bonsplit
import UserNotifications
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
@MainActor
func makeTemporaryBrowserProfile(named prefix: String) throws -> BrowserProfileDefinition {
try XCTUnwrap(
BrowserProfileStore.shared.createProfile(
named: "\(prefix)-\(UUID().uuidString)"
)
)
}
final class SidebarSelectedWorkspaceColorTests: XCTestCase {
func testLightModeUsesConfiguredSelectedWorkspaceBackgroundColor() {
guard let color = sidebarSelectedWorkspaceBackgroundNSColor(for: .light).usingColorSpace(.sRGB) else {
XCTFail("Expected sRGB-convertible color")
return
}
XCTAssertEqual(color.redComponent, 0, accuracy: 0.001)
XCTAssertEqual(color.greenComponent, 136.0 / 255.0, accuracy: 0.001)
XCTAssertEqual(color.blueComponent, 1.0, accuracy: 0.001)
XCTAssertEqual(color.alphaComponent, 1.0, accuracy: 0.001)
}
func testDarkModeUsesConfiguredSelectedWorkspaceBackgroundColor() {
guard let color = sidebarSelectedWorkspaceBackgroundNSColor(for: .dark).usingColorSpace(.sRGB) else {
XCTFail("Expected sRGB-convertible color")
return
}
XCTAssertEqual(color.redComponent, 0, accuracy: 0.001)
XCTAssertEqual(color.greenComponent, 145.0 / 255.0, accuracy: 0.001)
XCTAssertEqual(color.blueComponent, 1.0, accuracy: 0.001)
XCTAssertEqual(color.alphaComponent, 1.0, accuracy: 0.001)
}
func testSelectedWorkspaceForegroundAlwaysUsesWhiteWithRequestedOpacity() {
guard let color = sidebarSelectedWorkspaceForegroundNSColor(opacity: 0.65).usingColorSpace(.sRGB) else {
XCTFail("Expected sRGB-convertible color")
return
}
XCTAssertEqual(color.redComponent, 1.0, accuracy: 0.001)
XCTAssertEqual(color.greenComponent, 1.0, accuracy: 0.001)
XCTAssertEqual(color.blueComponent, 1.0, accuracy: 0.001)
XCTAssertEqual(color.alphaComponent, 0.65, accuracy: 0.001)
}
}
final class WorkspaceRenameShortcutDefaultsTests: XCTestCase {
func testRenameTabShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.renameTab.label, "Rename Tab")
XCTAssertEqual(KeyboardShortcutSettings.Action.renameTab.defaultsKey, "shortcut.renameTab")
let shortcut = KeyboardShortcutSettings.Action.renameTab.defaultShortcut
XCTAssertEqual(shortcut.key, "r")
XCTAssertTrue(shortcut.command)
XCTAssertFalse(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertFalse(shortcut.control)
}
func testCloseWindowShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWindow.label, "Close Window")
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWindow.defaultsKey, "shortcut.closeWindow")
let shortcut = KeyboardShortcutSettings.Action.closeWindow.defaultShortcut
XCTAssertEqual(shortcut.key, "w")
XCTAssertTrue(shortcut.command)
XCTAssertFalse(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertTrue(shortcut.control)
}
func testRenameWorkspaceShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.label, "Rename Workspace")
XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.defaultsKey, "shortcut.renameWorkspace")
let shortcut = KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut
XCTAssertEqual(shortcut.key, "r")
XCTAssertTrue(shortcut.command)
XCTAssertTrue(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertFalse(shortcut.control)
}
func testRenameWorkspaceShortcutConvertsToMenuShortcut() {
let shortcut = KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut
XCTAssertNotNil(shortcut.keyEquivalent)
XCTAssertTrue(shortcut.eventModifiers.contains(.command))
XCTAssertTrue(shortcut.eventModifiers.contains(.shift))
XCTAssertFalse(shortcut.eventModifiers.contains(.option))
XCTAssertFalse(shortcut.eventModifiers.contains(.control))
}
func testCloseWorkspaceShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWorkspace.label, "Close Workspace")
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWorkspace.defaultsKey, "shortcut.closeWorkspace")
let shortcut = KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut
XCTAssertEqual(shortcut.key, "w")
XCTAssertTrue(shortcut.command)
XCTAssertTrue(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertFalse(shortcut.control)
}
func testCloseWorkspaceShortcutConvertsToMenuShortcut() {
let shortcut = KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut
XCTAssertNotNil(shortcut.keyEquivalent)
XCTAssertTrue(shortcut.eventModifiers.contains(.command))
XCTAssertTrue(shortcut.eventModifiers.contains(.shift))
XCTAssertFalse(shortcut.eventModifiers.contains(.option))
XCTAssertFalse(shortcut.eventModifiers.contains(.control))
}
func testNextPreviousWorkspaceShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.nextSidebarTab.label, "Next Workspace")
XCTAssertEqual(KeyboardShortcutSettings.Action.prevSidebarTab.label, "Previous Workspace")
XCTAssertEqual(KeyboardShortcutSettings.Action.nextSidebarTab.defaultsKey, "shortcut.nextSidebarTab")
XCTAssertEqual(KeyboardShortcutSettings.Action.prevSidebarTab.defaultsKey, "shortcut.prevSidebarTab")
let nextShortcut = KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut
XCTAssertEqual(nextShortcut.key, "]")
XCTAssertTrue(nextShortcut.command)
XCTAssertFalse(nextShortcut.shift)
XCTAssertFalse(nextShortcut.option)
XCTAssertTrue(nextShortcut.control)
let prevShortcut = KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut
XCTAssertEqual(prevShortcut.key, "[")
XCTAssertTrue(prevShortcut.command)
XCTAssertFalse(prevShortcut.shift)
XCTAssertFalse(prevShortcut.option)
XCTAssertTrue(prevShortcut.control)
}
func testNextPreviousWorkspaceShortcutsConvertToMenuShortcut() {
let nextShortcut = KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut
XCTAssertNotNil(nextShortcut.keyEquivalent)
XCTAssertEqual(nextShortcut.menuItemKeyEquivalent, "]")
XCTAssertTrue(nextShortcut.eventModifiers.contains(.command))
XCTAssertTrue(nextShortcut.eventModifiers.contains(.control))
let prevShortcut = KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut
XCTAssertNotNil(prevShortcut.keyEquivalent)
XCTAssertEqual(prevShortcut.menuItemKeyEquivalent, "[")
XCTAssertTrue(prevShortcut.eventModifiers.contains(.command))
XCTAssertTrue(prevShortcut.eventModifiers.contains(.control))
}
func testToggleTerminalCopyModeShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.toggleTerminalCopyMode.label, "Toggle Terminal Copy Mode")
XCTAssertEqual(
KeyboardShortcutSettings.Action.toggleTerminalCopyMode.defaultsKey,
"shortcut.toggleTerminalCopyMode"
)
let shortcut = KeyboardShortcutSettings.Action.toggleTerminalCopyMode.defaultShortcut
XCTAssertEqual(shortcut.key, "m")
XCTAssertTrue(shortcut.command)
XCTAssertTrue(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertFalse(shortcut.control)
}
func testMenuItemKeyEquivalentHandlesArrowAndTabKeys() {
XCTAssertNotNil(StoredShortcut(key: "", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
XCTAssertNotNil(StoredShortcut(key: "", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
XCTAssertNotNil(StoredShortcut(key: "", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
XCTAssertNotNil(StoredShortcut(key: "", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
XCTAssertEqual(
StoredShortcut(key: "\t", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent,
"\t"
)
}
func testShortcutDefaultsKeysRemainUnique() {
let keys = KeyboardShortcutSettings.Action.allCases.map(\.defaultsKey)
XCTAssertEqual(Set(keys).count, keys.count)
}
}
final class WorkspaceShortcutMapperTests: XCTestCase {
func testCommandNineMapsToLastWorkspaceIndex() {
XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forDigit: 9, workspaceCount: 1), 0)
XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forDigit: 9, workspaceCount: 4), 3)
XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forDigit: 9, workspaceCount: 12), 11)
}
func testCommandDigitBadgesUseNineForLastWorkspaceWhenNeeded() {
XCTAssertEqual(WorkspaceShortcutMapper.digitForWorkspace(at: 0, workspaceCount: 12), 1)
XCTAssertEqual(WorkspaceShortcutMapper.digitForWorkspace(at: 7, workspaceCount: 12), 8)
XCTAssertEqual(WorkspaceShortcutMapper.digitForWorkspace(at: 11, workspaceCount: 12), 9)
XCTAssertNil(WorkspaceShortcutMapper.digitForWorkspace(at: 8, workspaceCount: 12))
}
}
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)
}
}
@MainActor
final class WorkspaceCreationPlacementTests: XCTestCase {
func testAddWorkspaceDefaultPlacementMatchesCurrentSetting() {
let currentPlacement = WorkspacePlacementSettings.current()
let defaultManager = makeManagerWithThreeWorkspaces()
let defaultBaselineOrder = defaultManager.tabs.map(\.id)
let defaultInserted = defaultManager.addWorkspace()
guard let defaultInsertedIndex = defaultManager.tabs.firstIndex(where: { $0.id == defaultInserted.id }) else {
XCTFail("Expected inserted workspace in tab list")
return
}
XCTAssertEqual(defaultManager.tabs.map(\.id).filter { $0 != defaultInserted.id }, defaultBaselineOrder)
let explicitManager = makeManagerWithThreeWorkspaces()
let explicitBaselineOrder = explicitManager.tabs.map(\.id)
let explicitInserted = explicitManager.addWorkspace(placementOverride: currentPlacement)
guard let explicitInsertedIndex = explicitManager.tabs.firstIndex(where: { $0.id == explicitInserted.id }) else {
XCTFail("Expected inserted workspace in tab list")
return
}
XCTAssertEqual(explicitManager.tabs.map(\.id).filter { $0 != explicitInserted.id }, explicitBaselineOrder)
XCTAssertEqual(defaultInsertedIndex, explicitInsertedIndex)
}
func testAddWorkspaceEndOverrideAlwaysAppends() {
let manager = makeManagerWithThreeWorkspaces()
let baselineCount = manager.tabs.count
guard baselineCount >= 3 else {
XCTFail("Expected at least three workspaces for placement regression test")
return
}
let inserted = manager.addWorkspace(placementOverride: .end)
guard let insertedIndex = manager.tabs.firstIndex(where: { $0.id == inserted.id }) else {
XCTFail("Expected inserted workspace in tab list")
return
}
XCTAssertEqual(insertedIndex, baselineCount)
}
func testAddWorkspaceAfterCurrentOverrideAppendsAfterLastSelectedWorkspace() {
let manager = TabManager()
guard !manager.tabs.isEmpty else {
XCTFail("Expected TabManager to initialise with at least one workspace")
return
}
_ = manager.addWorkspace()
_ = manager.addWorkspace()
let fourth = manager.addWorkspace()
let baselineOrder = manager.tabs.map(\.id)
manager.selectWorkspace(fourth)
let inserted = manager.addWorkspace(placementOverride: .afterCurrent)
XCTAssertEqual(manager.tabs.map(\.id).filter { $0 != inserted.id }, baselineOrder)
XCTAssertEqual(manager.tabs.last?.id, inserted.id)
}
private func makeManagerWithThreeWorkspaces() -> TabManager {
let manager = TabManager()
_ = manager.addWorkspace()
_ = manager.addWorkspace()
if let first = manager.tabs.first {
manager.selectWorkspace(first)
}
return manager
}
}
final class WorkspaceTabColorSettingsTests: XCTestCase {
func testNormalizedHexAcceptsAndNormalizesValidInput() {
XCTAssertEqual(WorkspaceTabColorSettings.normalizedHex("#abc123"), "#ABC123")
XCTAssertEqual(WorkspaceTabColorSettings.normalizedHex(" aBcDeF "), "#ABCDEF")
XCTAssertNil(WorkspaceTabColorSettings.normalizedHex("#1234"))
XCTAssertNil(WorkspaceTabColorSettings.normalizedHex("#GG1234"))
}
func testBuiltInPaletteMatchesOriginalPRPalette() {
let suiteName = "WorkspaceTabColorSettingsTests.BuiltInPalette.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
let palette = WorkspaceTabColorSettings.defaultPaletteWithOverrides(defaults: defaults)
XCTAssertEqual(palette.count, 16)
XCTAssertEqual(palette.first?.name, "Red")
XCTAssertEqual(palette.first?.hex, "#C0392B")
XCTAssertEqual(palette.last?.name, "Charcoal")
XCTAssertFalse(palette.contains(where: { $0.name == "Gold" }))
}
func testDefaultOverrideRoundTripFallsBackWhenResetToBase() {
let suiteName = "WorkspaceTabColorSettingsTests.DefaultOverride.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
let first = WorkspaceTabColorSettings.defaultPalette[0]
XCTAssertEqual(
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
first.hex
)
WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: "#00aa33", defaults: defaults)
XCTAssertEqual(
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
"#00AA33"
)
WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: first.hex, defaults: defaults)
XCTAssertEqual(
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
first.hex
)
}
func testAddCustomColorPersistsAndDeduplicatesByMostRecent() {
let suiteName = "WorkspaceTabColorSettingsTests.CustomColors.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
XCTAssertEqual(
WorkspaceTabColorSettings.addCustomColor(" #00aa33 ", defaults: defaults),
"#00AA33"
)
XCTAssertEqual(
WorkspaceTabColorSettings.addCustomColor("#112233", defaults: defaults),
"#112233"
)
XCTAssertEqual(
WorkspaceTabColorSettings.addCustomColor("#00AA33", defaults: defaults),
"#00AA33"
)
XCTAssertNil(WorkspaceTabColorSettings.addCustomColor("nope", defaults: defaults))
XCTAssertEqual(
WorkspaceTabColorSettings.customColors(defaults: defaults),
["#00AA33", "#112233"]
)
}
func testPaletteIncludesCustomEntriesAndResetClearsAll() {
let suiteName = "WorkspaceTabColorSettingsTests.Reset.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
let first = WorkspaceTabColorSettings.defaultPalette[0]
WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: "#334455", defaults: defaults)
_ = WorkspaceTabColorSettings.addCustomColor("#778899", defaults: defaults)
let paletteBeforeReset = WorkspaceTabColorSettings.palette(defaults: defaults)
XCTAssertEqual(paletteBeforeReset.count, WorkspaceTabColorSettings.defaultPalette.count + 1)
XCTAssertEqual(paletteBeforeReset[0].hex, "#334455")
XCTAssertEqual(paletteBeforeReset.last?.name, "Custom 1")
XCTAssertEqual(paletteBeforeReset.last?.hex, "#778899")
WorkspaceTabColorSettings.reset(defaults: defaults)
XCTAssertEqual(WorkspaceTabColorSettings.customColors(defaults: defaults), [])
XCTAssertEqual(
WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults),
first.hex
)
}
func testDisplayColorLightModeKeepsOriginalHex() {
let originalHex = "#1A5276"
let rendered = WorkspaceTabColorSettings.displayNSColor(
hex: originalHex,
colorScheme: .light
)
XCTAssertEqual(rendered?.hexString(), originalHex)
}
func testDisplayColorDarkModeBrightensColor() {
let originalHex = "#1A5276"
guard let base = NSColor(hex: originalHex),
let rendered = WorkspaceTabColorSettings.displayNSColor(
hex: originalHex,
colorScheme: .dark
) else {
XCTFail("Expected valid color conversion")
return
}
XCTAssertNotEqual(rendered.hexString(), originalHex)
XCTAssertGreaterThan(rendered.luminance, base.luminance)
}
func testDisplayColorDarkModeKeepsGrayscaleNeutral() {
let originalHex = "#808080"
guard let base = NSColor(hex: originalHex),
let rendered = WorkspaceTabColorSettings.displayNSColor(
hex: originalHex,
colorScheme: .dark
),
let renderedSRGB = rendered.usingColorSpace(.sRGB) else {
XCTFail("Expected valid color conversion")
return
}
XCTAssertGreaterThan(rendered.luminance, base.luminance)
XCTAssertLessThan(abs(renderedSRGB.redComponent - renderedSRGB.greenComponent), 0.003)
XCTAssertLessThan(abs(renderedSRGB.greenComponent - renderedSRGB.blueComponent), 0.003)
}
func testDisplayColorForceBrightensInLightMode() {
let originalHex = "#1A5276"
guard let base = NSColor(hex: originalHex),
let rendered = WorkspaceTabColorSettings.displayNSColor(
hex: originalHex,
colorScheme: .light,
forceBright: true
) else {
XCTFail("Expected valid color conversion")
return
}
XCTAssertNotEqual(rendered.hexString(), originalHex)
XCTAssertGreaterThan(rendered.luminance, base.luminance)
}
}
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 SidebarWorkspaceDetailSettingsTests: XCTestCase {
func testDefaultPreferencesWhenUnset() {
let suiteName = "SidebarWorkspaceDetailSettingsTests.Default.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
XCTAssertFalse(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults))
XCTAssertTrue(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults))
XCTAssertTrue(
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults),
hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)
)
)
}
func testStoredPreferencesOverrideDefaults() {
let suiteName = "SidebarWorkspaceDetailSettingsTests.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(true, forKey: SidebarWorkspaceDetailSettings.hideAllDetailsKey)
defaults.set(false, forKey: SidebarWorkspaceDetailSettings.showNotificationMessageKey)
XCTAssertTrue(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults))
XCTAssertFalse(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults))
XCTAssertFalse(
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults),
hideAllDetails: false
)
)
XCTAssertFalse(
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
showNotificationMessage: true,
hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)
)
)
}
}
final class SidebarWorkspaceAuxiliaryDetailVisibilityTests: XCTestCase {
func testResolvedVisibilityPreservesPerRowTogglesWhenDetailsAreShown() {
XCTAssertEqual(
SidebarWorkspaceAuxiliaryDetailVisibility.resolved(
showMetadata: true,
showLog: false,
showProgress: true,
showBranchDirectory: false,
showPullRequests: true,
showPorts: false,
hideAllDetails: false
),
SidebarWorkspaceAuxiliaryDetailVisibility(
showsMetadata: true,
showsLog: false,
showsProgress: true,
showsBranchDirectory: false,
showsPullRequests: true,
showsPorts: false
)
)
}
func testResolvedVisibilityHidesAllAuxiliaryRowsWhenDetailsAreHidden() {
XCTAssertEqual(
SidebarWorkspaceAuxiliaryDetailVisibility.resolved(
showMetadata: true,
showLog: true,
showProgress: true,
showBranchDirectory: true,
showPullRequests: true,
showPorts: true,
hideAllDetails: true
),
.hidden
)
}
}
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
func testReorderWorkspaceKeepsUnpinnedWorkspaceBelowPinnedSegment() {
let manager = TabManager()
let firstPinned = manager.tabs[0]
manager.setPinned(firstPinned, pinned: true)
let secondPinned = manager.addWorkspace()
manager.setPinned(secondPinned, pinned: true)
let unpinned = manager.addWorkspace()
XCTAssertTrue(manager.reorderWorkspace(tabId: unpinned.id, toIndex: 0))
XCTAssertEqual(manager.tabs.map(\.id), [firstPinned.id, secondPinned.id, unpinned.id])
}
@MainActor
func testReorderWorkspaceKeepsPinnedWorkspaceInsidePinnedSegment() {
let manager = TabManager()
let firstPinned = manager.tabs[0]
manager.setPinned(firstPinned, pinned: true)
let secondPinned = manager.addWorkspace()
manager.setPinned(secondPinned, pinned: true)
let unpinned = manager.addWorkspace()
XCTAssertTrue(manager.reorderWorkspace(tabId: firstPinned.id, toIndex: 999))
XCTAssertEqual(manager.tabs.map(\.id), [secondPinned.id, firstPinned.id, unpinned.id])
}
}
@MainActor
final class WorkspaceNotificationReorderTests: XCTestCase {
func testNotificationAutoReorderDoesNotMovePinnedWorkspace() {
let appDelegate = AppDelegate.shared ?? AppDelegate()
let manager = TabManager()
let notificationStore = TerminalNotificationStore.shared
let originalTabManager = appDelegate.tabManager
let originalNotificationStore = appDelegate.notificationStore
let defaults = UserDefaults.standard
let originalAutoReorderSetting = defaults.object(forKey: WorkspaceAutoReorderSettings.key)
let originalAppFocusOverride = AppFocusState.overrideIsFocused
notificationStore.replaceNotificationsForTesting([])
notificationStore.configureNotificationDeliveryHandlerForTesting { _, _ in }
appDelegate.tabManager = manager
appDelegate.notificationStore = notificationStore
defaults.set(true, forKey: WorkspaceAutoReorderSettings.key)
AppFocusState.overrideIsFocused = false
defer {
notificationStore.replaceNotificationsForTesting([])
notificationStore.resetNotificationDeliveryHandlerForTesting()
appDelegate.tabManager = originalTabManager
appDelegate.notificationStore = originalNotificationStore
AppFocusState.overrideIsFocused = originalAppFocusOverride
if let originalAutoReorderSetting {
defaults.set(originalAutoReorderSetting, forKey: WorkspaceAutoReorderSettings.key)
} else {
defaults.removeObject(forKey: WorkspaceAutoReorderSettings.key)
}
}
let firstPinned = manager.tabs[0]
manager.setPinned(firstPinned, pinned: true)
let secondPinned = manager.addWorkspace()
manager.setPinned(secondPinned, pinned: true)
let unpinned = manager.addWorkspace()
let expectedOrder = [firstPinned.id, secondPinned.id, unpinned.id]
notificationStore.addNotification(
tabId: secondPinned.id,
surfaceId: nil,
title: "Build finished",
subtitle: "",
body: "Pinned workspaces should stay put"
)
XCTAssertEqual(manager.tabs.map(\.id), expectedOrder)
}
}
@MainActor
final class WorkspaceTeardownTests: XCTestCase {
func testTeardownAllPanelsClearsPanelMetadataCaches() {
let workspace = Workspace()
guard let initialPanelId = workspace.focusedPanelId else {
XCTFail("Expected focused panel in new workspace")
return
}
workspace.setPanelCustomTitle(panelId: initialPanelId, title: "Initial custom title")
workspace.setPanelPinned(panelId: initialPanelId, pinned: true)
guard let splitPanel = workspace.newTerminalSplit(from: initialPanelId, orientation: .horizontal) else {
XCTFail("Expected split panel to be created")
return
}
workspace.setPanelCustomTitle(panelId: splitPanel.id, title: "Split custom title")
workspace.setPanelPinned(panelId: splitPanel.id, pinned: true)
workspace.markPanelUnread(initialPanelId)
XCTAssertFalse(workspace.panels.isEmpty)
XCTAssertFalse(workspace.panelTitles.isEmpty)
XCTAssertFalse(workspace.panelCustomTitles.isEmpty)
XCTAssertFalse(workspace.pinnedPanelIds.isEmpty)
XCTAssertFalse(workspace.manualUnreadPanelIds.isEmpty)
workspace.teardownAllPanels()
XCTAssertTrue(workspace.panels.isEmpty)
XCTAssertTrue(workspace.panelTitles.isEmpty)
XCTAssertTrue(workspace.panelCustomTitles.isEmpty)
XCTAssertTrue(workspace.pinnedPanelIds.isEmpty)
XCTAssertTrue(workspace.manualUnreadPanelIds.isEmpty)
}
}
@MainActor
final class WorkspaceSplitWorkingDirectoryTests: XCTestCase {
func testNewTerminalSplitFallsBackToRequestedWorkingDirectoryWhenReportedDirectoryIsStale() {
let workspace = Workspace()
guard let sourcePaneId = workspace.bonsplitController.focusedPaneId else {
XCTFail("Expected focused pane in new workspace")
return
}
let staleCurrentDirectory = workspace.currentDirectory
let requestedDirectory = "/tmp/cmux-requested-split-cwd-\(UUID().uuidString)"
guard let sourcePanel = workspace.newTerminalSurface(
inPane: sourcePaneId,
focus: false,
workingDirectory: requestedDirectory
) else {
XCTFail("Expected source terminal panel to be created")
return
}
XCTAssertEqual(sourcePanel.requestedWorkingDirectory, requestedDirectory)
XCTAssertNil(
workspace.panelDirectories[sourcePanel.id],
"Expected requested cwd to exist before shell integration reports a live cwd"
)
XCTAssertEqual(
workspace.currentDirectory,
staleCurrentDirectory,
"Expected focused workspace cwd to remain stale before panel directory updates"
)
guard let splitPanel = workspace.newTerminalSplit(
from: sourcePanel.id,
orientation: .horizontal,
focus: false
) else {
XCTFail("Expected split terminal panel to be created")
return
}
XCTAssertEqual(
splitPanel.requestedWorkingDirectory,
requestedDirectory,
"Expected split to inherit the source terminal's requested cwd when no reported cwd exists yet"
)
}
}
@MainActor
final class WorkspaceTerminalFocusRecoveryTests: XCTestCase {
private func makeWindow() -> NSWindow {
NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 360, height: 220),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
}
private func makeMouseEvent(
type: NSEvent.EventType,
location: NSPoint,
window: NSWindow
) -> NSEvent {
guard let event = NSEvent.mouseEvent(
with: type,
location: location,
modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: window.windowNumber,
context: nil,
eventNumber: 0,
clickCount: 1,
pressure: 1.0
) else {
fatalError("Failed to create \(type) mouse event")
}
return event
}
private func surfaceView(in hostedView: GhosttySurfaceScrollView) -> GhosttyNSView? {
var stack: [NSView] = [hostedView]
while let current = stack.popLast() {
if let surfaceView = current as? GhosttyNSView {
return surfaceView
}
stack.append(contentsOf: current.subviews)
}
return nil
}
func testTerminalFirstResponderConvergesSplitActiveStateWhenSelectionAlreadyMatches() {
let workspace = Workspace()
guard let leftPanelId = workspace.focusedPanelId,
let leftPanel = workspace.terminalPanel(for: leftPanelId),
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected split terminal panels")
return
}
XCTAssertEqual(
workspace.focusedPanelId,
rightPanel.id,
"Expected the new split panel to be selected before simulating stale focus state"
)
// Simulate the split-pane failure mode: Bonsplit already points at the right panel,
// but the active terminal state is still stale on the left panel.
leftPanel.surface.setFocus(true)
leftPanel.hostedView.setActive(true)
rightPanel.surface.setFocus(false)
rightPanel.hostedView.setActive(false)
workspace.focusPanel(rightPanel.id, trigger: .terminalFirstResponder)
XCTAssertFalse(
leftPanel.hostedView.debugRenderStats().isActive,
"Expected stale left-pane active state to be cleared"
)
XCTAssertTrue(
rightPanel.hostedView.debugRenderStats().isActive,
"Expected terminal-first-responder recovery to reactivate the selected split pane"
)
}
func testTerminalClickRecoversSplitActiveStateWhenFocusCallbackIsSuppressed() {
let workspace = Workspace()
guard let leftPanelId = workspace.focusedPanelId,
let leftPanel = workspace.terminalPanel(for: leftPanelId),
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected split terminal panels")
return
}
let window = makeWindow()
defer { window.orderOut(nil) }
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
leftPanel.hostedView.frame = NSRect(x: 0, y: 0, width: 180, height: 220)
rightPanel.hostedView.frame = NSRect(x: 180, y: 0, width: 180, height: 220)
contentView.addSubview(leftPanel.hostedView)
contentView.addSubview(rightPanel.hostedView)
leftPanel.hostedView.setVisibleInUI(true)
rightPanel.hostedView.setVisibleInUI(true)
leftPanel.hostedView.setFocusHandler {
workspace.focusPanel(leftPanel.id, trigger: .terminalFirstResponder)
}
rightPanel.hostedView.setFocusHandler {
workspace.focusPanel(rightPanel.id, trigger: .terminalFirstResponder)
}
window.makeKeyAndOrderFront(nil)
window.displayIfNeeded()
contentView.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
XCTAssertEqual(
workspace.focusedPanelId,
rightPanel.id,
"Expected the clicked split pane to already be selected before simulating stale focus state"
)
// Simulate the ghost-terminal race: the right pane is selected in Bonsplit, but stale
// active state remains on the left and the right pane's AppKit focus callback never fires
// after split reparent/layout churn.
leftPanel.surface.setFocus(true)
leftPanel.hostedView.setActive(true)
rightPanel.surface.setFocus(false)
rightPanel.hostedView.setActive(false)
rightPanel.hostedView.suppressReparentFocus()
guard let rightSurfaceView = surfaceView(in: rightPanel.hostedView) else {
XCTFail("Expected right terminal surface view")
return
}
let pointInWindow = rightSurfaceView.convert(NSPoint(x: 24, y: 24), to: nil)
let event = makeMouseEvent(type: .leftMouseDown, location: pointInWindow, window: window)
rightSurfaceView.mouseDown(with: event)
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
XCTAssertFalse(
leftPanel.hostedView.debugRenderStats().isActive,
"Expected clicking the selected split pane to clear stale sibling active state even when AppKit focus callbacks are suppressed"
)
XCTAssertTrue(
rightPanel.hostedView.debugRenderStats().isActive,
"Expected clicking the selected split pane to reactivate terminal input when focus callbacks are suppressed"
)
XCTAssertTrue(
rightPanel.hostedView.isSurfaceViewFirstResponder(),
"Expected the clicked split pane to become first responder"
)
}
}
@MainActor
final class WorkspaceTerminalConfigInheritanceSelectionTests: XCTestCase {
func testPrefersSelectedTerminalInTargetPaneOverFocusedTerminalElsewhere() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let leftPanelId = workspace.focusedPanelId,
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal),
let leftPaneId = workspace.paneId(forPanelId: leftPanelId) else {
XCTFail("Expected workspace split setup to succeed")
return
}
// Programmatic split focuses the new right panel by default.
XCTAssertEqual(workspace.focusedPanelId, rightPanel.id)
let sourcePanel = workspace.terminalPanelForConfigInheritance(inPane: leftPaneId)
XCTAssertEqual(
sourcePanel?.id,
leftPanelId,
"Expected inheritance to use the selected terminal in the target pane"
)
}
func testFallsBackToAnotherTerminalInPaneWhenSelectedTabIsBrowser() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let terminalPanelId = workspace.focusedPanelId,
let paneId = workspace.paneId(forPanelId: terminalPanelId),
let browserPanel = workspace.newBrowserSurface(inPane: paneId, focus: true) else {
XCTFail("Expected workspace browser setup to succeed")
return
}
XCTAssertEqual(workspace.focusedPanelId, browserPanel.id)
let sourcePanel = workspace.terminalPanelForConfigInheritance(inPane: paneId)
XCTAssertEqual(
sourcePanel?.id,
terminalPanelId,
"Expected inheritance to fall back to a terminal in the pane when browser is selected"
)
}
func testPreferredTerminalPanelWinsWhenProvided() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let terminalPanelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with a terminal panel")
return
}
let sourcePanel = workspace.terminalPanelForConfigInheritance(preferredPanelId: terminalPanelId)
XCTAssertEqual(sourcePanel?.id, terminalPanelId)
}
func testPrefersLastFocusedTerminalWhenBrowserFocusedInDifferentPane() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let leftTerminalPanelId = workspace.focusedPanelId,
let rightTerminalPanel = workspace.newTerminalSplit(from: leftTerminalPanelId, orientation: .horizontal),
let rightPaneId = workspace.paneId(forPanelId: rightTerminalPanel.id) else {
XCTFail("Expected split setup to succeed")
return
}
workspace.focusPanel(leftTerminalPanelId)
_ = workspace.newBrowserSurface(inPane: rightPaneId, focus: true)
XCTAssertNotEqual(workspace.focusedPanelId, leftTerminalPanelId)
let sourcePanel = workspace.terminalPanelForConfigInheritance(inPane: rightPaneId)
XCTAssertEqual(
sourcePanel?.id,
leftTerminalPanelId,
"Expected inheritance to prefer last focused terminal when browser is focused in another pane"
)
}
}
@MainActor
final class WorkspaceAttentionFlashTests: XCTestCase {
func testMoveFocusDoesNotTriggerWholePaneFlashTokenWhenWholePaneModeEnabled() {
let defaults = UserDefaults.standard
let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey)
let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey)
defer {
if let originalExperimentEnabled {
defaults.set(originalExperimentEnabled, forKey: TmuxOverlayExperimentSettings.enabledKey)
} else {
defaults.removeObject(forKey: TmuxOverlayExperimentSettings.enabledKey)
}
if let originalExperimentTarget {
defaults.set(originalExperimentTarget, forKey: TmuxOverlayExperimentSettings.targetKey)
} else {
defaults.removeObject(forKey: TmuxOverlayExperimentSettings.targetKey)
}
}
defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey)
defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey)
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let leftPanelId = workspace.focusedPanelId,
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected split terminal panels")
return
}
XCTAssertEqual(workspace.focusedPanelId, rightPanel.id)
XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 0)
XCTAssertNil(workspace.tmuxWorkspaceFlashPanelId)
workspace.moveFocus(direction: .left)
XCTAssertEqual(workspace.focusedPanelId, leftPanelId)
XCTAssertEqual(
workspace.tmuxWorkspaceFlashToken,
0,
"Expected moving focus left to avoid any workspace-pane flash"
)
XCTAssertNil(workspace.tmuxWorkspaceFlashPanelId)
workspace.moveFocus(direction: .right)
XCTAssertEqual(workspace.focusedPanelId, rightPanel.id)
XCTAssertEqual(
workspace.tmuxWorkspaceFlashToken,
0,
"Expected moving focus right to avoid any workspace-pane flash"
)
XCTAssertNil(workspace.tmuxWorkspaceFlashPanelId)
}
func testMoveFocusSuppressesWorkspacePaneFlashWhenAnotherPaneOwnsUnreadAttention() {
let appDelegate = AppDelegate.shared ?? AppDelegate()
let manager = TabManager()
let notificationStore = TerminalNotificationStore.shared
let defaults = UserDefaults.standard
let originalTabManager = appDelegate.tabManager
let originalNotificationStore = appDelegate.notificationStore
let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey)
let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey)
let originalAppFocusOverride = AppFocusState.overrideIsFocused
defer {
notificationStore.replaceNotificationsForTesting([])
notificationStore.resetNotificationDeliveryHandlerForTesting()
appDelegate.tabManager = originalTabManager
appDelegate.notificationStore = originalNotificationStore
AppFocusState.overrideIsFocused = originalAppFocusOverride
if let originalExperimentEnabled {
defaults.set(originalExperimentEnabled, forKey: TmuxOverlayExperimentSettings.enabledKey)
} else {
defaults.removeObject(forKey: TmuxOverlayExperimentSettings.enabledKey)
}
if let originalExperimentTarget {
defaults.set(originalExperimentTarget, forKey: TmuxOverlayExperimentSettings.targetKey)
} else {
defaults.removeObject(forKey: TmuxOverlayExperimentSettings.targetKey)
}
}
notificationStore.replaceNotificationsForTesting([])
notificationStore.configureNotificationDeliveryHandlerForTesting { _, _ in }
appDelegate.tabManager = manager
appDelegate.notificationStore = notificationStore
AppFocusState.overrideIsFocused = true
defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey)
defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey)
guard let workspace = manager.selectedWorkspace,
let leftPanelId = workspace.focusedPanelId,
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected split terminal panels")
return
}
workspace.moveFocus(direction: .left)
notificationStore.addNotification(
tabId: workspace.id,
surfaceId: leftPanelId,
title: "Unread",
subtitle: "",
body: "Left pane owns notification attention"
)
XCTAssertTrue(
notificationStore.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: leftPanelId),
"Expected the left pane to own visible notification attention before moving focus"
)
let flashTokenBeforeNavigation = workspace.tmuxWorkspaceFlashToken
workspace.moveFocus(direction: .right)
XCTAssertEqual(workspace.focusedPanelId, rightPanel.id)
XCTAssertEqual(
workspace.tmuxWorkspaceFlashToken,
flashTokenBeforeNavigation,
"Expected navigation flash to be suppressed while another pane owns notification attention"
)
}
}
@MainActor
final class WorkspaceBrowserProfileSelectionTests: XCTestCase {
private final class RejectingCreateTabDelegate: BonsplitDelegate {
func splitTabBar(_ controller: BonsplitController, shouldCreateTab tab: Bonsplit.Tab, inPane pane: PaneID) -> Bool {
false
}
}
private final class RejectingSplitPaneDelegate: BonsplitDelegate {
func splitTabBar(_ controller: BonsplitController, shouldSplitPane pane: PaneID, orientation: SplitOrientation) -> Bool {
false
}
}
func testNewBrowserSurfacePrefersSelectedBrowserProfileInTargetPane() throws {
let workspace = Workspace()
let profileA = try makeTemporaryBrowserProfile(named: "Alpha")
let profileB = try makeTemporaryBrowserProfile(named: "Beta")
let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId)
let browserA = try XCTUnwrap(
workspace.newBrowserSurface(
inPane: paneId,
focus: true,
preferredProfileID: profileA.id
)
)
_ = try XCTUnwrap(
workspace.newBrowserSplit(
from: browserA.id,
orientation: .horizontal,
preferredProfileID: profileB.id,
focus: true
)
)
XCTAssertEqual(
workspace.preferredBrowserProfileID,
profileB.id,
"Expected workspace preference to drift to the most recently created browser profile"
)
let leftSurfaceId = try XCTUnwrap(workspace.surfaceIdFromPanelId(browserA.id))
workspace.bonsplitController.focusPane(paneId)
workspace.bonsplitController.selectTab(leftSurfaceId)
let created = try XCTUnwrap(
workspace.newBrowserSurface(
inPane: paneId,
focus: false
)
)
XCTAssertEqual(
created.profileID,
profileA.id,
"Expected new browser creation to inherit the selected browser profile from the target pane"
)
}
func testNewBrowserSurfaceFailureDoesNotMutatePreferredProfile() throws {
let workspace = Workspace()
let preferredProfile = try makeTemporaryBrowserProfile(named: "Preferred")
let unexpectedProfile = try makeTemporaryBrowserProfile(named: "Unexpected")
let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId)
_ = try XCTUnwrap(
workspace.newBrowserSurface(
inPane: paneId,
focus: false,
preferredProfileID: preferredProfile.id
)
)
XCTAssertEqual(workspace.preferredBrowserProfileID, preferredProfile.id)
let rejectingDelegate = RejectingCreateTabDelegate()
workspace.bonsplitController.delegate = rejectingDelegate
let created = workspace.newBrowserSurface(
inPane: paneId,
focus: false,
preferredProfileID: unexpectedProfile.id
)
XCTAssertNil(created)
XCTAssertEqual(
workspace.preferredBrowserProfileID,
preferredProfile.id,
"Expected a failed browser creation to leave the workspace preferred profile unchanged"
)
}
func testNewBrowserSplitFailureDoesNotMutatePreferredProfile() throws {
let workspace = Workspace()
let preferredProfile = try makeTemporaryBrowserProfile(named: "Preferred")
let unexpectedProfile = try makeTemporaryBrowserProfile(named: "Unexpected")
let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId)
let browser = try XCTUnwrap(
workspace.newBrowserSurface(
inPane: paneId,
focus: true,
preferredProfileID: preferredProfile.id
)
)
XCTAssertEqual(workspace.preferredBrowserProfileID, preferredProfile.id)
let rejectingDelegate = RejectingSplitPaneDelegate()
workspace.bonsplitController.delegate = rejectingDelegate
let created = workspace.newBrowserSplit(
from: browser.id,
orientation: .horizontal,
preferredProfileID: unexpectedProfile.id,
focus: false
)
XCTAssertNil(created)
XCTAssertEqual(
workspace.preferredBrowserProfileID,
preferredProfile.id,
"Expected a failed browser split to leave the workspace preferred profile unchanged"
)
}
}
@MainActor
final class WorkspacePanelGitBranchTests: XCTestCase {
private func drainMainQueue() {
let expectation = expectation(description: "drain main queue")
DispatchQueue.main.async {
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
func testBrowserSplitWithFocusFalsePreservesOriginalFocusedPanel() {
let workspace = Workspace()
guard let originalFocusedPanelId = workspace.focusedPanelId else {
XCTFail("Expected initial focused panel")
return
}
guard let browserSplitPanel = workspace.newBrowserSplit(
from: originalFocusedPanelId,
orientation: .horizontal,
focus: false
) else {
XCTFail("Expected browser split panel to be created")
return
}
drainMainQueue()
XCTAssertNotEqual(browserSplitPanel.id, originalFocusedPanelId)
XCTAssertEqual(
workspace.focusedPanelId,
originalFocusedPanelId,
"Expected non-focus browser split to preserve pre-split focus"
)
}
func testTerminalSplitWithFocusFalsePreservesOriginalFocusedPanel() {
let workspace = Workspace()
guard let originalFocusedPanelId = workspace.focusedPanelId else {
XCTFail("Expected initial focused panel")
return
}
guard let terminalSplitPanel = workspace.newTerminalSplit(
from: originalFocusedPanelId,
orientation: .horizontal,
focus: false
) else {
XCTFail("Expected terminal split panel to be created")
return
}
drainMainQueue()
XCTAssertNotEqual(terminalSplitPanel.id, originalFocusedPanelId)
XCTAssertEqual(
workspace.focusedPanelId,
originalFocusedPanelId,
"Expected non-focus terminal split to preserve pre-split focus"
)
}
func testDetachLastSurfaceLeavesWorkspaceTemporarilyEmptyForMoveFlow() {
let workspace = Workspace()
guard let panelId = workspace.focusedPanelId,
let paneId = workspace.paneId(forPanelId: panelId) else {
XCTFail("Expected initial panel and pane")
return
}
XCTAssertEqual(workspace.panels.count, 1)
#if DEBUG
let baselineFocusReconcileDuringDetach = workspace.debugFocusReconcileScheduledDuringDetachCount
#endif
guard let detached = workspace.detachSurface(panelId: panelId) else {
XCTFail("Expected detach of last surface to succeed")
return
}
XCTAssertEqual(detached.panelId, panelId)
XCTAssertTrue(
workspace.panels.isEmpty,
"Detaching the last surface should not auto-create a replacement panel"
)
XCTAssertNil(workspace.surfaceIdFromPanelId(panelId))
XCTAssertEqual(workspace.bonsplitController.tabs(inPane: paneId).count, 0)
drainMainQueue()
drainMainQueue()
#if DEBUG
XCTAssertEqual(
workspace.debugFocusReconcileScheduledDuringDetachCount,
baselineFocusReconcileDuringDetach,
"Detaching during cross-workspace moves should not schedule delayed source focus reconciliation"
)
#endif
let restoredPanelId = workspace.attachDetachedSurface(detached, inPane: paneId, focus: false)
XCTAssertEqual(restoredPanelId, panelId)
XCTAssertEqual(workspace.panels.count, 1)
}
func testDetachSurfaceWithRemainingPanelsSkipsDelayedFocusReconcile() {
let workspace = Workspace()
guard let originalPanelId = workspace.focusedPanelId,
let movedPanel = workspace.newTerminalSplit(from: originalPanelId, orientation: .horizontal) else {
XCTFail("Expected two panels before detach")
return
}
drainMainQueue()
drainMainQueue()
#if DEBUG
let baselineFocusReconcileDuringDetach = workspace.debugFocusReconcileScheduledDuringDetachCount
#endif
guard let detached = workspace.detachSurface(panelId: movedPanel.id) else {
XCTFail("Expected detach to succeed")
return
}
XCTAssertEqual(detached.panelId, movedPanel.id)
XCTAssertEqual(workspace.panels.count, 1, "Expected source workspace to retain only the surviving panel")
XCTAssertNotNil(workspace.panels[originalPanelId], "Expected the original panel to remain after detach")
drainMainQueue()
drainMainQueue()
#if DEBUG
XCTAssertEqual(
workspace.debugFocusReconcileScheduledDuringDetachCount,
baselineFocusReconcileDuringDetach,
"Detaching into another workspace should not enqueue delayed source focus reconciliation"
)
#endif
}
func testDetachAttachAcrossWorkspacesPreservesNonCustomPanelTitle() {
let source = Workspace()
guard let panelId = source.focusedPanelId else {
XCTFail("Expected source focused panel")
return
}
XCTAssertTrue(source.updatePanelTitle(panelId: panelId, title: "detached-runtime-title"))
guard let detached = source.detachSurface(panelId: panelId) else {
XCTFail("Expected detach to succeed")
return
}
XCTAssertEqual(detached.cachedTitle, "detached-runtime-title")
XCTAssertNil(detached.customTitle)
XCTAssertEqual(
detached.title,
"detached-runtime-title",
"Detached transfer should carry the cached non-custom title"
)
let destination = Workspace()
guard let destinationPane = destination.bonsplitController.allPaneIds.first else {
XCTFail("Expected destination pane")
return
}
let attachedPanelId = destination.attachDetachedSurface(
detached,
inPane: destinationPane,
focus: false
)
XCTAssertEqual(attachedPanelId, panelId)
XCTAssertEqual(destination.panelTitle(panelId: panelId), "detached-runtime-title")
guard let attachedTabId = destination.surfaceIdFromPanelId(panelId),
let attachedTab = destination.bonsplitController.tab(attachedTabId) else {
XCTFail("Expected attached tab mapping")
return
}
XCTAssertEqual(attachedTab.title, "detached-runtime-title")
XCTAssertFalse(attachedTab.hasCustomTitle)
}
func testBrowserSplitWithFocusFalseRecoversFromDelayedStaleSelection() {
let workspace = Workspace()
guard let originalFocusedPanelId = workspace.focusedPanelId else {
XCTFail("Expected initial focused panel")
return
}
guard let originalPaneId = workspace.paneId(forPanelId: originalFocusedPanelId) else {
XCTFail("Expected focused pane for initial panel")
return
}
guard let browserSplitPanel = workspace.newBrowserSplit(
from: originalFocusedPanelId,
orientation: .horizontal,
focus: false
) else {
XCTFail("Expected browser split panel to be created")
return
}
guard let splitPaneId = workspace.paneId(forPanelId: browserSplitPanel.id),
let splitTabId = workspace.surfaceIdFromPanelId(browserSplitPanel.id),
let splitTab = workspace.bonsplitController
.tabs(inPane: splitPaneId)
.first(where: { $0.id == splitTabId }) else {
XCTFail("Expected split pane/tab mapping")
return
}
// Simulate one delayed stale split-selection callback from bonsplit.
DispatchQueue.main.async {
workspace.splitTabBar(workspace.bonsplitController, didSelectTab: splitTab, inPane: splitPaneId)
}
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(
workspace.focusedPanelId,
originalFocusedPanelId,
"Expected non-focus split to reassert the pre-split focused panel"
)
XCTAssertEqual(
workspace.bonsplitController.focusedPaneId,
originalPaneId,
"Expected focused pane to converge back to the pre-split pane"
)
XCTAssertEqual(
workspace.bonsplitController.selectedTab(inPane: originalPaneId)?.id,
workspace.surfaceIdFromPanelId(originalFocusedPanelId),
"Expected selected tab to converge back to the pre-split focused panel"
)
}
func testBrowserSplitWithFocusFalseAllowsSubsequentExplicitFocusOnSplitPanel() {
let workspace = Workspace()
guard let originalFocusedPanelId = workspace.focusedPanelId else {
XCTFail("Expected initial focused panel")
return
}
guard let browserSplitPanel = workspace.newBrowserSplit(
from: originalFocusedPanelId,
orientation: .horizontal,
focus: false
) else {
XCTFail("Expected browser split panel to be created")
return
}
workspace.focusPanel(browserSplitPanel.id)
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(
workspace.focusedPanelId,
browserSplitPanel.id,
"Expected explicit focus intent to keep the split panel focused"
)
}
func testNewTerminalSurfaceWithFocusFalsePreservesFocusedPanel() {
let workspace = Workspace()
guard let originalFocusedPanelId = workspace.focusedPanelId,
let originalPaneId = workspace.paneId(forPanelId: originalFocusedPanelId) else {
XCTFail("Expected initial focused panel and pane")
return
}
guard let newPanel = workspace.newTerminalSurface(inPane: originalPaneId, focus: false) else {
XCTFail("Expected terminal surface to be created")
return
}
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertNotEqual(newPanel.id, originalFocusedPanelId)
XCTAssertEqual(
workspace.focusedPanelId,
originalFocusedPanelId,
"Expected non-focus terminal surface creation to preserve the existing focused panel"
)
XCTAssertEqual(
workspace.bonsplitController.selectedTab(inPane: originalPaneId)?.id,
workspace.surfaceIdFromPanelId(originalFocusedPanelId),
"Expected selected tab to stay on the original focused panel"
)
}
func testNewBrowserSurfaceWithFocusFalsePreservesFocusedPanel() {
let workspace = Workspace()
guard let originalFocusedPanelId = workspace.focusedPanelId,
let originalPaneId = workspace.paneId(forPanelId: originalFocusedPanelId) else {
XCTFail("Expected initial focused panel and pane")
return
}
guard let newPanel = workspace.newBrowserSurface(inPane: originalPaneId, focus: false) else {
XCTFail("Expected browser surface to be created")
return
}
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertNotEqual(newPanel.id, originalFocusedPanelId)
XCTAssertEqual(
workspace.focusedPanelId,
originalFocusedPanelId,
"Expected non-focus browser surface creation to preserve the existing focused panel"
)
XCTAssertEqual(
workspace.bonsplitController.selectedTab(inPane: originalPaneId)?.id,
workspace.surfaceIdFromPanelId(originalFocusedPanelId),
"Expected selected tab to stay on the original focused panel"
)
}
func testClosingFocusedSplitRestoresBranchForRemainingFocusedPanel() {
let workspace = Workspace()
guard let firstPanelId = workspace.focusedPanelId else {
XCTFail("Expected initial focused panel")
return
}
workspace.updatePanelGitBranch(panelId: firstPanelId, branch: "main", isDirty: false)
guard let secondPanel = workspace.newTerminalSplit(from: firstPanelId, orientation: .horizontal) else {
XCTFail("Expected split panel to be created")
return
}
workspace.updatePanelGitBranch(panelId: secondPanel.id, branch: "feature/bugfix", isDirty: true)
XCTAssertEqual(workspace.focusedPanelId, secondPanel.id, "Expected split panel to be focused")
XCTAssertEqual(workspace.gitBranch?.branch, "feature/bugfix")
XCTAssertEqual(workspace.gitBranch?.isDirty, true)
XCTAssertTrue(workspace.closePanel(secondPanel.id, force: true), "Expected split panel close to succeed")
XCTAssertEqual(workspace.focusedPanelId, firstPanelId, "Expected surviving panel to become focused")
XCTAssertEqual(workspace.gitBranch?.branch, "main")
XCTAssertEqual(workspace.gitBranch?.isDirty, false)
}
func testSidebarGitBranchesFollowLeftToRightSplitOrder() {
let workspace = Workspace()
guard let leftPanelId = workspace.focusedPanelId else {
XCTFail("Expected initial focused panel")
return
}
workspace.updatePanelGitBranch(panelId: leftPanelId, branch: "main", isDirty: false)
guard let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected split panel to be created")
return
}
workspace.updatePanelGitBranch(panelId: rightPanel.id, branch: "feature/sidebar", isDirty: true)
let ordered = workspace.sidebarGitBranchesInDisplayOrder()
XCTAssertEqual(ordered.map(\.branch), ["main", "feature/sidebar"])
XCTAssertEqual(ordered.map(\.isDirty), [false, true])
}
@MainActor
func testSidebarPullRequestsTrackFocusedPanelOnly() {
let workspace = Workspace()
guard let firstPanelId = workspace.focusedPanelId,
let paneId = workspace.paneId(forPanelId: firstPanelId),
let secondPanel = workspace.newTerminalSurface(inPane: paneId, focus: false) else {
XCTFail("Expected focused panel and a second panel")
return
}
workspace.updatePanelGitBranch(panelId: firstPanelId, branch: "main", isDirty: false)
workspace.updatePanelGitBranch(panelId: secondPanel.id, branch: "feature/sidebar-pr", isDirty: false)
workspace.updatePanelPullRequest(
panelId: secondPanel.id,
number: 1629,
label: "PR",
url: URL(string: "https://github.com/manaflow-ai/cmux/pull/1629")!,
status: .open
)
XCTAssertNil(workspace.pullRequest)
XCTAssertTrue(
workspace.sidebarPullRequestsInDisplayOrder().isEmpty,
"Expected background panel PRs to stay hidden while the focused panel has no PR"
)
workspace.focusPanel(secondPanel.id)
XCTAssertEqual(
workspace.sidebarPullRequestsInDisplayOrder().map(\.number),
[1629]
)
}
func testSidebarOrderingUsesPaneOrderThenTabOrderWithBranchDeduping() {
let workspace = Workspace()
guard let leftFirstPanelId = workspace.focusedPanelId,
let leftPaneId = workspace.paneId(forPanelId: leftFirstPanelId),
let rightFirstPanel = workspace.newTerminalSplit(from: leftFirstPanelId, orientation: .horizontal),
let rightPaneId = workspace.paneId(forPanelId: rightFirstPanel.id),
let leftSecondPanel = workspace.newTerminalSurface(inPane: leftPaneId, focus: false),
let rightSecondPanel = workspace.newTerminalSurface(inPane: rightPaneId, focus: false) else {
XCTFail("Expected panes and panels for ordering test")
return
}
XCTAssertTrue(workspace.reorderSurface(panelId: leftFirstPanelId, toIndex: 0))
XCTAssertTrue(workspace.reorderSurface(panelId: leftSecondPanel.id, toIndex: 1))
XCTAssertTrue(workspace.reorderSurface(panelId: rightFirstPanel.id, toIndex: 0))
XCTAssertTrue(workspace.reorderSurface(panelId: rightSecondPanel.id, toIndex: 1))
workspace.updatePanelGitBranch(panelId: leftFirstPanelId, branch: "main", isDirty: false)
workspace.updatePanelGitBranch(panelId: leftSecondPanel.id, branch: "feature/left", isDirty: false)
workspace.updatePanelGitBranch(panelId: rightFirstPanel.id, branch: "main", isDirty: true)
workspace.updatePanelGitBranch(panelId: rightSecondPanel.id, branch: "feature/right", isDirty: false)
XCTAssertEqual(
workspace.sidebarOrderedPanelIds(),
[leftFirstPanelId, leftSecondPanel.id, rightFirstPanel.id, rightSecondPanel.id]
)
let branches = workspace.sidebarGitBranchesInDisplayOrder()
XCTAssertEqual(branches.map(\.branch), ["main", "feature/left", "feature/right"])
XCTAssertEqual(branches.map(\.isDirty), [true, false, false])
}
func testSidebarBranchDirectoryEntriesStayStableAcrossFocusedSplitChanges() {
let workspace = Workspace()
let leftLiveDirectory = "/repo/left/live"
let rightFocusedDirectory = "/repo/right/focused"
let leftFocusedDirectory = "/repo/left/focused"
let rightRequestedDirectory = "/repo/right/requested"
guard let leftPanelId = workspace.focusedPanelId else {
XCTFail("Expected initial focused panel")
return
}
workspace.updatePanelDirectory(panelId: leftPanelId, directory: leftLiveDirectory)
guard let rightSplitPanel = workspace.newTerminalSplit(
from: leftPanelId,
orientation: .horizontal,
focus: false
),
let rightPaneId = workspace.paneId(forPanelId: rightSplitPanel.id),
let rightRequestedPanel = workspace.newTerminalSurface(
inPane: rightPaneId,
focus: false,
workingDirectory: rightRequestedDirectory
) else {
XCTFail("Expected right split panes for sidebar directory ordering test")
return
}
let orderedPanelIds = workspace.sidebarOrderedPanelIds()
XCTAssertEqual(orderedPanelIds, [leftPanelId, rightSplitPanel.id, rightRequestedPanel.id])
workspace.currentDirectory = rightFocusedDirectory
let entriesWhenRightLooksFocused = workspace.sidebarBranchDirectoryEntriesInDisplayOrder(
orderedPanelIds: orderedPanelIds
)
workspace.currentDirectory = leftFocusedDirectory
let entriesWhenLeftLooksFocused = workspace.sidebarBranchDirectoryEntriesInDisplayOrder(
orderedPanelIds: orderedPanelIds
)
XCTAssertEqual(
entriesWhenRightLooksFocused,
entriesWhenLeftLooksFocused,
"Expected sidebar directory ordering to ignore focused-workspace cwd churn when panel-specific directories are available"
)
XCTAssertEqual(
entriesWhenRightLooksFocused.map(\.directory),
[leftLiveDirectory, rightRequestedDirectory]
)
}
func testRemoteSidebarDirectoryCanonicalizationDedupesTildeAndAbsoluteHomePaths() {
let workspace = Workspace()
workspace.configureRemoteConnection(
WorkspaceRemoteConfiguration(
destination: "cmux-macmini",
port: nil,
identityFile: nil,
sshOptions: [],
localProxyPort: nil,
relayPort: 64007,
relayID: String(repeating: "a", count: 16),
relayToken: String(repeating: "b", count: 64),
localSocketPath: "/tmp/cmux-debug-test.sock",
terminalStartupCommand: "ssh cmux-macmini"
),
autoConnect: false
)
let liveDirectory = "/home/remoteuser/project"
let requestedDirectory = "~/project"
guard let firstPanelId = workspace.focusedPanelId,
let paneId = workspace.paneId(forPanelId: firstPanelId),
let requestedPanel = workspace.newTerminalSurface(
inPane: paneId,
focus: false,
workingDirectory: requestedDirectory
) else {
XCTFail("Expected remote panels for sidebar directory canonicalization test")
return
}
workspace.updatePanelDirectory(panelId: firstPanelId, directory: liveDirectory)
let orderedPanelIds = workspace.sidebarOrderedPanelIds()
XCTAssertEqual(orderedPanelIds, [firstPanelId, requestedPanel.id])
XCTAssertEqual(
workspace.sidebarDirectoriesInDisplayOrder(orderedPanelIds: orderedPanelIds),
[liveDirectory]
)
XCTAssertEqual(
workspace.sidebarBranchDirectoryEntriesInDisplayOrder(orderedPanelIds: orderedPanelIds).map(\.directory),
[liveDirectory]
)
}
func testSidebarDerivedCollectionsMatchWhenUsingPrecomputedPanelOrder() {
let workspace = Workspace()
guard let leftFirstPanelId = workspace.focusedPanelId,
let leftPaneId = workspace.paneId(forPanelId: leftFirstPanelId),
let rightFirstPanel = workspace.newTerminalSplit(from: leftFirstPanelId, orientation: .horizontal),
let rightPaneId = workspace.paneId(forPanelId: rightFirstPanel.id),
let leftSecondPanel = workspace.newTerminalSurface(inPane: leftPaneId, focus: false),
let rightSecondPanel = workspace.newTerminalSurface(inPane: rightPaneId, focus: false) else {
XCTFail("Expected panes and panels for precomputed ordering test")
return
}
workspace.updatePanelGitBranch(panelId: leftFirstPanelId, branch: "main", isDirty: false)
workspace.updatePanelGitBranch(panelId: leftSecondPanel.id, branch: "feature/left", isDirty: true)
workspace.updatePanelGitBranch(panelId: rightFirstPanel.id, branch: "release/right", isDirty: false)
workspace.updatePanelDirectory(panelId: leftFirstPanelId, directory: "/repo/left/root")
workspace.updatePanelDirectory(panelId: leftSecondPanel.id, directory: "/repo/left/feature")
workspace.updatePanelDirectory(panelId: rightFirstPanel.id, directory: "/repo/right/root")
workspace.updatePanelDirectory(panelId: rightSecondPanel.id, directory: "/repo/right/extra")
workspace.updatePanelPullRequest(
panelId: leftFirstPanelId,
number: 101,
label: "PR",
url: URL(string: "https://github.com/manaflow-ai/cmux/pull/101")!,
status: .open
)
workspace.updatePanelPullRequest(
panelId: rightFirstPanel.id,
number: 18,
label: "MR",
url: URL(string: "https://gitlab.com/manaflow/cmux/-/merge_requests/18")!,
status: .merged
)
let orderedPanelIds = workspace.sidebarOrderedPanelIds()
XCTAssertEqual(
workspace.sidebarGitBranchesInDisplayOrder(orderedPanelIds: orderedPanelIds).map { "\($0.branch)|\($0.isDirty)" },
workspace.sidebarGitBranchesInDisplayOrder().map { "\($0.branch)|\($0.isDirty)" }
)
XCTAssertEqual(
workspace.sidebarBranchDirectoryEntriesInDisplayOrder(orderedPanelIds: orderedPanelIds),
workspace.sidebarBranchDirectoryEntriesInDisplayOrder()
)
XCTAssertEqual(
workspace.sidebarPullRequestsInDisplayOrder(orderedPanelIds: orderedPanelIds),
workspace.sidebarPullRequestsInDisplayOrder()
)
}
func testClosingPaneDropsBranchesFromClosedSide() {
let workspace = Workspace()
guard let leftPanelId = workspace.focusedPanelId,
let leftPaneId = workspace.paneId(forPanelId: leftPanelId),
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected left/right split panes")
return
}
workspace.updatePanelGitBranch(panelId: leftPanelId, branch: "branch1", isDirty: false)
workspace.updatePanelGitBranch(panelId: rightPanel.id, branch: "branch2", isDirty: false)
XCTAssertEqual(workspace.sidebarGitBranchesInDisplayOrder().map(\.branch), ["branch1", "branch2"])
XCTAssertTrue(workspace.bonsplitController.closePane(leftPaneId))
XCTAssertEqual(workspace.sidebarGitBranchesInDisplayOrder().map(\.branch), ["branch2"])
}
}
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 SidebarWorkspaceShortcutHintMetricsTests: XCTestCase {
override func setUp() {
super.setUp()
SidebarWorkspaceShortcutHintMetrics.resetCacheForTesting()
}
override func tearDown() {
SidebarWorkspaceShortcutHintMetrics.resetCacheForTesting()
super.tearDown()
}
func testHintWidthCachesRepeatedMeasurements() {
XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 0)
let first = SidebarWorkspaceShortcutHintMetrics.hintWidth(for: "⌘1")
XCTAssertGreaterThan(first, 0)
XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 1)
let second = SidebarWorkspaceShortcutHintMetrics.hintWidth(for: "⌘1")
XCTAssertEqual(second, first)
XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 1)
_ = SidebarWorkspaceShortcutHintMetrics.hintWidth(for: "⌘2")
XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 2)
}
func testSlotWidthAppliesMinimumAndDebugInset() {
let nilLabelWidth = SidebarWorkspaceShortcutHintMetrics.slotWidth(label: nil, debugXOffset: 999)
XCTAssertEqual(nilLabelWidth, 28)
let base = SidebarWorkspaceShortcutHintMetrics.slotWidth(label: "⌘1", debugXOffset: 0)
let widened = SidebarWorkspaceShortcutHintMetrics.slotWidth(label: "⌘1", debugXOffset: 10)
XCTAssertGreaterThan(widened, base)
}
}