cmux/cmuxTests/TabManagerUnitTests.swift
2026-03-20 20:18:33 -07:00

1080 lines
42 KiB
Swift

import XCTest
import AppKit
import SwiftUI
import UniformTypeIdentifiers
import WebKit
import ObjectiveC.runtime
import Bonsplit
import UserNotifications
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
let lastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut"
func drainMainQueue() {
let expectation = XCTestExpectation(description: "drain main queue")
DispatchQueue.main.async {
expectation.fulfill()
}
XCTWaiter().wait(for: [expectation], timeout: 1.0)
}
@MainActor
final class TabManagerChildExitCloseTests: XCTestCase {
func testChildExitOnLastPanelClosesSelectedWorkspaceAndKeepsIndexStable() {
let manager = TabManager()
let first = manager.tabs[0]
let second = manager.addWorkspace()
let third = manager.addWorkspace()
manager.selectWorkspace(second)
XCTAssertEqual(manager.selectedTabId, second.id)
guard let secondPanelId = second.focusedPanelId else {
XCTFail("Expected focused panel in selected workspace")
return
}
manager.closePanelAfterChildExited(tabId: second.id, surfaceId: secondPanelId)
XCTAssertEqual(manager.tabs.map(\.id), [first.id, third.id])
XCTAssertEqual(
manager.selectedTabId,
third.id,
"Expected selection to stay at the same index after deleting the selected workspace"
)
}
func testChildExitOnLastPanelInLastWorkspaceSelectsPreviousWorkspace() {
let manager = TabManager()
let first = manager.tabs[0]
let second = manager.addWorkspace()
manager.selectWorkspace(second)
XCTAssertEqual(manager.selectedTabId, second.id)
guard let secondPanelId = second.focusedPanelId else {
XCTFail("Expected focused panel in selected workspace")
return
}
manager.closePanelAfterChildExited(tabId: second.id, surfaceId: secondPanelId)
XCTAssertEqual(manager.tabs.map(\.id), [first.id])
XCTAssertEqual(
manager.selectedTabId,
first.id,
"Expected previous workspace to be selected after closing the last-index workspace"
)
}
func testChildExitOnNonLastPanelClosesOnlyPanel() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let initialPanelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with focused panel")
return
}
guard let splitPanel = workspace.newTerminalSplit(from: initialPanelId, orientation: .horizontal) else {
XCTFail("Expected split terminal panel to be created")
return
}
let panelCountBefore = workspace.panels.count
manager.closePanelAfterChildExited(tabId: workspace.id, surfaceId: splitPanel.id)
XCTAssertEqual(manager.tabs.count, 1)
XCTAssertEqual(manager.tabs.first?.id, workspace.id)
XCTAssertEqual(workspace.panels.count, panelCountBefore - 1)
XCTAssertNotNil(workspace.panels[initialPanelId], "Expected sibling panel to remain")
}
}
@MainActor
final class TabManagerWorkspaceOwnershipTests: XCTestCase {
func testCloseWorkspaceIgnoresWorkspaceNotOwnedByManager() {
let manager = TabManager()
_ = manager.addWorkspace()
let initialTabIds = manager.tabs.map(\.id)
let initialSelectedTabId = manager.selectedTabId
let externalWorkspace = Workspace(title: "External workspace")
let externalPanelCountBefore = externalWorkspace.panels.count
let externalPanelTitlesBefore = externalWorkspace.panelTitles
manager.closeWorkspace(externalWorkspace)
XCTAssertEqual(manager.tabs.map(\.id), initialTabIds)
XCTAssertEqual(manager.selectedTabId, initialSelectedTabId)
XCTAssertEqual(externalWorkspace.panels.count, externalPanelCountBefore)
XCTAssertEqual(externalWorkspace.panelTitles, externalPanelTitlesBefore)
}
}
@MainActor
final class TabManagerCloseWorkspacesWithConfirmationTests: XCTestCase {
func testCloseWorkspacesWithConfirmationPromptsOnceAndClosesAcceptedWorkspaces() {
let manager = TabManager()
let second = manager.addWorkspace()
let third = manager.addWorkspace()
manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
manager.setCustomTitle(tabId: second.id, title: "Beta")
manager.setCustomTitle(tabId: third.id, title: "Gamma")
var prompts: [(title: String, message: String, acceptCmdD: Bool)] = []
manager.confirmCloseHandler = { title, message, acceptCmdD in
prompts.append((title, message, acceptCmdD))
return true
}
manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true)
let expectedMessage = String(
format: String(
localized: "dialog.closeWorkspaces.message",
defaultValue: "This will close %1$lld workspaces and all of their panels:\n%2$@"
),
locale: .current,
Int64(2),
"• Alpha\n• Beta"
)
XCTAssertEqual(prompts.count, 1, "Expected a single confirmation prompt for multi-close")
XCTAssertEqual(
prompts.first?.title,
String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?")
)
XCTAssertEqual(prompts.first?.message, expectedMessage)
XCTAssertEqual(prompts.first?.acceptCmdD, false)
XCTAssertEqual(manager.tabs.map(\.title), ["Gamma"])
}
func testCloseWorkspacesWithConfirmationKeepsWorkspacesWhenCancelled() {
let manager = TabManager()
let second = manager.addWorkspace()
manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
manager.setCustomTitle(tabId: second.id, title: "Beta")
var prompts: [(title: String, message: String, acceptCmdD: Bool)] = []
manager.confirmCloseHandler = { title, message, acceptCmdD in
prompts.append((title, message, acceptCmdD))
return false
}
manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true)
let expectedMessage = String(
format: String(
localized: "dialog.closeWorkspacesWindow.message",
defaultValue: "This will close the current window, its %1$lld workspaces, and all of their panels:\n%2$@"
),
locale: .current,
Int64(2),
"• Alpha\n• Beta"
)
XCTAssertEqual(prompts.count, 1)
XCTAssertEqual(
prompts.first?.title,
String(localized: "dialog.closeWindow.title", defaultValue: "Close window?")
)
XCTAssertEqual(prompts.first?.message, expectedMessage)
XCTAssertEqual(prompts.first?.acceptCmdD, true)
XCTAssertEqual(manager.tabs.map(\.title), ["Alpha", "Beta"])
}
func testCloseCurrentWorkspaceWithConfirmationUsesSidebarMultiSelection() {
let manager = TabManager()
let second = manager.addWorkspace()
let third = manager.addWorkspace()
manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
manager.setCustomTitle(tabId: second.id, title: "Beta")
manager.setCustomTitle(tabId: third.id, title: "Gamma")
manager.selectWorkspace(second)
manager.setSidebarSelectedWorkspaceIds([manager.tabs[0].id, second.id])
var prompts: [(title: String, message: String, acceptCmdD: Bool)] = []
manager.confirmCloseHandler = { title, message, acceptCmdD in
prompts.append((title, message, acceptCmdD))
return false
}
manager.closeCurrentWorkspaceWithConfirmation()
let expectedMessage = String(
format: String(
localized: "dialog.closeWorkspaces.message",
defaultValue: "This will close %1$lld workspaces and all of their panels:\n%2$@"
),
locale: .current,
Int64(2),
"• Alpha\n• Beta"
)
XCTAssertEqual(prompts.count, 1, "Expected Cmd+Shift+W path to reuse the multi-close summary dialog")
XCTAssertEqual(
prompts.first?.title,
String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?")
)
XCTAssertEqual(prompts.first?.message, expectedMessage)
XCTAssertEqual(prompts.first?.acceptCmdD, false)
XCTAssertEqual(manager.tabs.map(\.title), ["Alpha", "Beta", "Gamma"])
}
}
@MainActor
final class TabManagerCloseCurrentPanelTests: XCTestCase {
func testRuntimeCloseSkipsConfirmationWhenShellReportsPromptIdle() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let panelId = workspace.focusedPanelId,
let terminalPanel = workspace.terminalPanel(for: panelId) else {
XCTFail("Expected selected workspace and focused terminal panel")
return
}
terminalPanel.surface.setNeedsConfirmCloseOverrideForTesting(true)
workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle)
var promptCount = 0
manager.confirmCloseHandler = { _, _, _ in
promptCount += 1
return false
}
manager.closeRuntimeSurfaceWithConfirmation(tabId: workspace.id, surfaceId: panelId)
drainMainQueue()
drainMainQueue()
XCTAssertEqual(promptCount, 0, "Runtime closes should honor prompt-idle shell state")
XCTAssertNil(workspace.panels[panelId], "Expected the original panel to close")
XCTAssertEqual(workspace.panels.count, 1, "Expected a replacement surface after closing the last panel")
}
func testRuntimeClosePromptsWhenShellReportsRunningCommand() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let panelId = workspace.focusedPanelId,
let terminalPanel = workspace.terminalPanel(for: panelId) else {
XCTFail("Expected selected workspace and focused terminal panel")
return
}
terminalPanel.surface.setNeedsConfirmCloseOverrideForTesting(false)
workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning)
var promptCount = 0
manager.confirmCloseHandler = { _, _, _ in
promptCount += 1
return false
}
manager.closeRuntimeSurfaceWithConfirmation(tabId: workspace.id, surfaceId: panelId)
XCTAssertEqual(promptCount, 1, "Running commands should still require confirmation")
XCTAssertNotNil(workspace.panels[panelId], "Prompt rejection should keep the original panel open")
}
func testCloseCurrentPanelClosesWorkspaceWhenItOwnsTheLastSurface() {
let manager = TabManager()
let firstWorkspace = manager.tabs[0]
let secondWorkspace = manager.addWorkspace()
manager.selectWorkspace(secondWorkspace)
guard let secondPanelId = secondWorkspace.focusedPanelId else {
XCTFail("Expected focused panel in selected workspace")
return
}
XCTAssertEqual(manager.selectedTabId, secondWorkspace.id)
XCTAssertEqual(secondWorkspace.panels.count, 1)
manager.closeCurrentPanelWithConfirmation()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id])
XCTAssertEqual(manager.selectedTabId, firstWorkspace.id)
XCTAssertNil(secondWorkspace.panels[secondPanelId])
XCTAssertTrue(secondWorkspace.panels.isEmpty)
}
func testCloseCurrentPanelKeepsWorkspaceOpenWhenKeepWorkspaceOpenPreferenceIsEnabled() {
let defaults = UserDefaults.standard
let originalSetting = defaults.object(forKey: lastSurfaceCloseShortcutDefaultsKey)
defaults.set(false, forKey: lastSurfaceCloseShortcutDefaultsKey)
defer {
if let originalSetting {
defaults.set(originalSetting, forKey: lastSurfaceCloseShortcutDefaultsKey)
} else {
defaults.removeObject(forKey: lastSurfaceCloseShortcutDefaultsKey)
}
}
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let initialPanelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace and focused panel")
return
}
let initialWorkspaceId = workspace.id
manager.closeCurrentPanelWithConfirmation()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.tabs.count, 1)
XCTAssertEqual(manager.selectedTabId, initialWorkspaceId)
XCTAssertEqual(manager.tabs.first?.id, initialWorkspaceId)
XCTAssertNil(workspace.panels[initialPanelId])
XCTAssertEqual(workspace.panels.count, 1)
XCTAssertNotEqual(workspace.focusedPanelId, initialPanelId)
}
func testClosePanelButtonClosesWorkspaceWhenItOwnsTheLastSurface() {
let manager = TabManager()
let firstWorkspace = manager.tabs[0]
let secondWorkspace = manager.addWorkspace()
manager.selectWorkspace(secondWorkspace)
guard let secondPanelId = secondWorkspace.focusedPanelId else {
XCTFail("Expected focused panel in selected workspace")
return
}
XCTAssertEqual(manager.selectedTabId, secondWorkspace.id)
XCTAssertEqual(secondWorkspace.panels.count, 1)
guard let secondSurfaceId = secondWorkspace.surfaceIdFromPanelId(secondPanelId) else {
XCTFail("Expected bonsplit surface ID for focused panel")
return
}
secondWorkspace.markExplicitClose(surfaceId: secondSurfaceId)
XCTAssertFalse(secondWorkspace.closePanel(secondPanelId))
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id])
XCTAssertEqual(manager.selectedTabId, firstWorkspace.id)
XCTAssertNil(secondWorkspace.panels[secondPanelId])
XCTAssertTrue(secondWorkspace.panels.isEmpty)
}
func testClosePanelButtonStillClosesWorkspaceWhenKeepWorkspaceOpenPreferenceIsEnabled() {
let defaults = UserDefaults.standard
let originalSetting = defaults.object(forKey: lastSurfaceCloseShortcutDefaultsKey)
defaults.set(false, forKey: lastSurfaceCloseShortcutDefaultsKey)
defer {
if let originalSetting {
defaults.set(originalSetting, forKey: lastSurfaceCloseShortcutDefaultsKey)
} else {
defaults.removeObject(forKey: lastSurfaceCloseShortcutDefaultsKey)
}
}
let manager = TabManager()
let firstWorkspace = manager.tabs[0]
let secondWorkspace = manager.addWorkspace()
manager.selectWorkspace(secondWorkspace)
guard let secondPanelId = secondWorkspace.focusedPanelId else {
XCTFail("Expected focused panel in selected workspace")
return
}
guard let secondSurfaceId = secondWorkspace.surfaceIdFromPanelId(secondPanelId) else {
XCTFail("Expected bonsplit surface ID for focused panel")
return
}
secondWorkspace.markExplicitClose(surfaceId: secondSurfaceId)
XCTAssertFalse(secondWorkspace.closePanel(secondPanelId))
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id])
XCTAssertEqual(manager.selectedTabId, firstWorkspace.id)
XCTAssertNil(secondWorkspace.panels[secondPanelId])
XCTAssertTrue(secondWorkspace.panels.isEmpty)
}
func testGenericClosePanelKeepsWorkspaceOpenWithoutExplicitCloseMarker() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let initialPanelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace and focused panel")
return
}
let initialWorkspaceId = workspace.id
XCTAssertEqual(manager.tabs.count, 1)
XCTAssertEqual(workspace.panels.count, 1)
XCTAssertTrue(workspace.closePanel(initialPanelId))
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.tabs.count, 1)
XCTAssertEqual(manager.selectedTabId, initialWorkspaceId)
XCTAssertEqual(manager.tabs.first?.id, initialWorkspaceId)
XCTAssertNil(workspace.panels[initialPanelId])
XCTAssertEqual(workspace.panels.count, 1)
XCTAssertNotEqual(workspace.focusedPanelId, initialPanelId)
}
func testCloseCurrentPanelIgnoresStaleSurfaceId() {
let manager = TabManager()
let firstWorkspace = manager.tabs[0]
let secondWorkspace = manager.addWorkspace()
manager.closePanelWithConfirmation(tabId: secondWorkspace.id, surfaceId: UUID())
XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id, secondWorkspace.id])
}
func testCloseCurrentPanelClearsNotificationsForClosedSurface() {
let appDelegate = AppDelegate.shared ?? AppDelegate()
let manager = TabManager()
let store = TerminalNotificationStore.shared
let originalTabManager = appDelegate.tabManager
let originalNotificationStore = appDelegate.notificationStore
store.replaceNotificationsForTesting([])
store.configureNotificationDeliveryHandlerForTesting { _, _ in }
appDelegate.tabManager = manager
appDelegate.notificationStore = store
defer {
store.replaceNotificationsForTesting([])
store.resetNotificationDeliveryHandlerForTesting()
appDelegate.tabManager = originalTabManager
appDelegate.notificationStore = originalNotificationStore
}
guard let workspace = manager.selectedWorkspace,
let initialPanelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace and focused panel")
return
}
store.addNotification(
tabId: workspace.id,
surfaceId: initialPanelId,
title: "Unread",
subtitle: "",
body: ""
)
XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: initialPanelId))
manager.closeCurrentPanelWithConfirmation()
drainMainQueue()
drainMainQueue()
XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: initialPanelId))
}
}
@MainActor
final class TabManagerNotificationFocusTests: XCTestCase {
func testFocusTabFromNotificationClearsSplitZoomBeforeFocusingTargetPanel() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let leftPanelId = workspace.focusedPanelId,
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected split setup to succeed")
return
}
workspace.focusPanel(leftPanelId)
XCTAssertTrue(workspace.toggleSplitZoom(panelId: leftPanelId), "Expected split zoom to enable")
XCTAssertTrue(workspace.bonsplitController.isSplitZoomed, "Expected workspace to start zoomed")
XCTAssertTrue(manager.focusTabFromNotification(workspace.id, surfaceId: rightPanel.id))
drainMainQueue()
drainMainQueue()
XCTAssertFalse(
workspace.bonsplitController.isSplitZoomed,
"Expected notification focus to exit split zoom so the target pane becomes visible"
)
XCTAssertEqual(workspace.focusedPanelId, rightPanel.id, "Expected notification target panel to be focused")
}
func testFocusTabFromNotificationReturnsFalseForMissingPanel() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace else {
XCTFail("Expected selected workspace")
return
}
XCTAssertFalse(manager.focusTabFromNotification(workspace.id, surfaceId: UUID()))
}
}
@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")
}
func testOpenBrowserInWorkspaceSplitRightSelectsTargetWorkspaceAndCreatesSplit() {
let manager = TabManager()
guard let initialWorkspace = manager.selectedWorkspace else {
XCTFail("Expected initial selected workspace")
return
}
guard let url = URL(string: "https://example.com/pull/123") else {
XCTFail("Expected test URL to be valid")
return
}
let targetWorkspace = manager.addWorkspace(select: false)
manager.selectWorkspace(initialWorkspace)
let initialPaneCount = targetWorkspace.bonsplitController.allPaneIds.count
let initialPanelCount = targetWorkspace.panels.count
guard let browserPanelId = manager.openBrowser(
inWorkspace: targetWorkspace.id,
url: url,
preferSplitRight: true,
insertAtEnd: true
) else {
XCTFail("Expected browser panel to be created in target workspace")
return
}
XCTAssertEqual(manager.selectedTabId, targetWorkspace.id, "Expected target workspace to become selected")
XCTAssertEqual(
targetWorkspace.bonsplitController.allPaneIds.count,
initialPaneCount + 1,
"Expected split-right browser open to create a new pane"
)
XCTAssertEqual(
targetWorkspace.panels.count,
initialPanelCount + 1,
"Expected browser panel count to increase by one"
)
XCTAssertEqual(
targetWorkspace.focusedPanelId,
browserPanelId,
"Expected created browser panel to be focused in target workspace"
)
XCTAssertTrue(
targetWorkspace.panels[browserPanelId] is BrowserPanel,
"Expected created panel to be a browser panel"
)
}
func testOpenBrowserInWorkspaceSplitRightReusesTopRightPaneWhenAlreadySplit() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let leftPanelId = workspace.focusedPanelId,
let topRightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal),
workspace.newTerminalSplit(from: topRightPanel.id, orientation: .vertical) != nil,
let topRightPaneId = workspace.paneId(forPanelId: topRightPanel.id),
let url = URL(string: "https://example.com/pull/456") else {
XCTFail("Expected split setup to succeed")
return
}
let initialPaneCount = workspace.bonsplitController.allPaneIds.count
guard let browserPanelId = manager.openBrowser(
inWorkspace: workspace.id,
url: url,
preferSplitRight: true,
insertAtEnd: true
) else {
XCTFail("Expected browser panel to be created")
return
}
XCTAssertEqual(
workspace.bonsplitController.allPaneIds.count,
initialPaneCount,
"Expected split-right browser open to reuse existing panes"
)
XCTAssertEqual(
workspace.paneId(forPanelId: browserPanelId),
topRightPaneId,
"Expected browser to open in the top-right pane when multiple splits already exist"
)
let targetPaneTabs = workspace.bonsplitController.tabs(inPane: topRightPaneId)
guard let lastSurfaceId = targetPaneTabs.last?.id else {
XCTFail("Expected top-right pane to contain tabs")
return
}
XCTAssertEqual(
workspace.panelIdFromSurfaceId(lastSurfaceId),
browserPanelId,
"Expected browser surface to be appended at end in the reused top-right pane"
)
}
}
@MainActor
final class TabManagerEqualizeSplitsTests: XCTestCase {
func testEqualizeSplitsSetsEverySplitDividerToHalf() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let leftPanelId = workspace.focusedPanelId,
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal),
workspace.newTerminalSplit(from: rightPanel.id, orientation: .vertical) != nil else {
XCTFail("Expected nested split setup to succeed")
return
}
let initialSplits = splitNodes(in: workspace.bonsplitController.treeSnapshot())
XCTAssertGreaterThanOrEqual(initialSplits.count, 2, "Expected at least two split nodes in nested layout")
for (index, split) in initialSplits.enumerated() {
guard let splitId = UUID(uuidString: split.id) else {
XCTFail("Expected split ID to be a UUID")
return
}
let targetPosition: CGFloat = index.isMultiple(of: 2) ? 0.2 : 0.8
XCTAssertTrue(
workspace.bonsplitController.setDividerPosition(targetPosition, forSplit: splitId),
"Expected to seed divider position for split \(splitId)"
)
}
XCTAssertTrue(manager.equalizeSplits(tabId: workspace.id), "Expected equalize splits command to succeed")
let equalizedSplits = splitNodes(in: workspace.bonsplitController.treeSnapshot())
XCTAssertEqual(equalizedSplits.count, initialSplits.count)
for split in equalizedSplits {
XCTAssertEqual(split.dividerPosition, 0.5, accuracy: 0.000_1)
}
}
private func splitNodes(in node: ExternalTreeNode) -> [ExternalSplitNode] {
switch node {
case .pane:
return []
case .split(let split):
return [split] + splitNodes(in: split.first) + splitNodes(in: split.second)
}
}
}
@MainActor
final class TabManagerWorkspaceConfigInheritanceSourceTests: XCTestCase {
func testUsesFocusedTerminalWhenTerminalIsFocused() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let terminalPanelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with focused terminal")
return
}
let sourcePanel = manager.terminalPanelForWorkspaceConfigInheritanceSource()
XCTAssertEqual(sourcePanel?.id, terminalPanelId)
}
func testFallsBackToTerminalWhenBrowserIsFocused() {
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 selected workspace setup to succeed")
return
}
XCTAssertEqual(workspace.focusedPanelId, browserPanel.id)
let sourcePanel = manager.terminalPanelForWorkspaceConfigInheritanceSource()
XCTAssertEqual(
sourcePanel?.id,
terminalPanelId,
"Expected new workspace inheritance source to resolve to the pane terminal when browser is focused"
)
}
func testPrefersLastFocusedTerminalAcrossPanesWhenBrowserIsFocused() {
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 = manager.terminalPanelForWorkspaceConfigInheritanceSource()
XCTAssertEqual(
sourcePanel?.id,
leftTerminalPanelId,
"Expected workspace inheritance source to use last focused terminal across panes"
)
}
}
@MainActor
final class TabManagerFocusedNotificationIndicatorTests: XCTestCase {
func testDismissNotificationOnDirectInteractionClearsFocusedNotificationIndicator() {
let appDelegate = AppDelegate.shared ?? AppDelegate()
let manager = TabManager()
let store = TerminalNotificationStore.shared
let originalTabManager = appDelegate.tabManager
let originalNotificationStore = appDelegate.notificationStore
let originalAppFocusOverride = AppFocusState.overrideIsFocused
store.replaceNotificationsForTesting([])
store.configureNotificationDeliveryHandlerForTesting { _, _ in }
appDelegate.tabManager = manager
appDelegate.notificationStore = store
AppFocusState.overrideIsFocused = true
defer {
store.replaceNotificationsForTesting([])
store.resetNotificationDeliveryHandlerForTesting()
appDelegate.tabManager = originalTabManager
appDelegate.notificationStore = originalNotificationStore
AppFocusState.overrideIsFocused = originalAppFocusOverride
}
guard let workspace = manager.selectedWorkspace,
let panelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with focused panel")
return
}
store.setFocusedReadIndicator(forTabId: workspace.id, surfaceId: panelId)
XCTAssertTrue(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId))
XCTAssertTrue(
manager.dismissNotificationOnDirectInteraction(tabId: workspace.id, surfaceId: panelId)
)
XCTAssertFalse(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId))
}
func testDismissNotificationOnDirectInteractionTriggersDismissFlashForFocusedIndicatorOnly() {
let appDelegate = AppDelegate.shared ?? AppDelegate()
let manager = TabManager()
let store = TerminalNotificationStore.shared
let defaults = UserDefaults.standard
let originalTabManager = appDelegate.tabManager
let originalNotificationStore = appDelegate.notificationStore
let originalAppFocusOverride = AppFocusState.overrideIsFocused
let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey)
let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey)
store.replaceNotificationsForTesting([])
store.configureNotificationDeliveryHandlerForTesting { _, _ in }
appDelegate.tabManager = manager
appDelegate.notificationStore = store
AppFocusState.overrideIsFocused = true
defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey)
defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey)
defer {
store.replaceNotificationsForTesting([])
store.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)
}
}
guard let workspace = manager.selectedWorkspace,
let panelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with focused panel")
return
}
store.setFocusedReadIndicator(forTabId: workspace.id, surfaceId: panelId)
XCTAssertTrue(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId))
XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: panelId))
XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 0)
XCTAssertTrue(
manager.dismissNotificationOnDirectInteraction(tabId: workspace.id, surfaceId: panelId)
)
XCTAssertFalse(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId))
XCTAssertEqual(
workspace.tmuxWorkspaceFlashToken,
1,
"Expected dismissing a focused-read indicator to emit a dismiss flash even when unread is already cleared"
)
XCTAssertEqual(workspace.tmuxWorkspaceFlashPanelId, panelId)
XCTAssertEqual(workspace.tmuxWorkspaceFlashReason, .notificationDismiss)
}
}
@MainActor
final class TabManagerReopenClosedBrowserFocusTests: XCTestCase {
func testReopenFromDifferentWorkspaceFocusesReopenedBrowser() {
let manager = TabManager()
guard let workspace1 = manager.selectedWorkspace,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/ws-switch")) else {
XCTFail("Expected initial workspace and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(workspace1.closePanel(closedBrowserId, force: true))
drainMainQueue()
let workspace2 = manager.addWorkspace()
XCTAssertEqual(manager.selectedTabId, workspace2.id)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace1.id)
XCTAssertTrue(isFocusedPanelBrowser(in: workspace1))
}
func testReopenFallsBackToCurrentWorkspaceAndFocusesBrowserWhenOriginalWorkspaceDeleted() {
let manager = TabManager()
guard let originalWorkspace = manager.selectedWorkspace,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/deleted-ws")) else {
XCTFail("Expected initial workspace and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(originalWorkspace.closePanel(closedBrowserId, force: true))
drainMainQueue()
let currentWorkspace = manager.addWorkspace()
manager.closeWorkspace(originalWorkspace)
XCTAssertEqual(manager.selectedTabId, currentWorkspace.id)
XCTAssertFalse(manager.tabs.contains(where: { $0.id == originalWorkspace.id }))
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, currentWorkspace.id)
XCTAssertTrue(isFocusedPanelBrowser(in: currentWorkspace))
}
func testReopenCollapsedSplitFromDifferentWorkspaceFocusesBrowser() {
let manager = TabManager()
guard let workspace1 = manager.selectedWorkspace,
let sourcePanelId = workspace1.focusedPanelId,
let splitBrowserId = manager.newBrowserSplit(
tabId: workspace1.id,
fromPanelId: sourcePanelId,
orientation: .horizontal,
insertFirst: false,
url: URL(string: "https://example.com/collapsed-split")
) else {
XCTFail("Expected to create browser split")
return
}
drainMainQueue()
XCTAssertTrue(workspace1.closePanel(splitBrowserId, force: true))
drainMainQueue()
let workspace2 = manager.addWorkspace()
XCTAssertEqual(manager.selectedTabId, workspace2.id)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace1.id)
XCTAssertTrue(isFocusedPanelBrowser(in: workspace1))
}
func testReopenFromDifferentWorkspaceWinsAgainstSingleDeferredStaleFocus() {
let manager = TabManager()
guard let workspace1 = manager.selectedWorkspace,
let preReopenPanelId = workspace1.focusedPanelId,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/stale-focus-cross-ws")) else {
XCTFail("Expected initial workspace state and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(workspace1.closePanel(closedBrowserId, force: true))
drainMainQueue()
let panelIdsBeforeReopen = Set(workspace1.panels.keys)
let workspace2 = manager.addWorkspace()
XCTAssertEqual(manager.selectedTabId, workspace2.id)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
guard let reopenedPanelId = singleNewPanelId(in: workspace1, comparedTo: panelIdsBeforeReopen) else {
XCTFail("Expected reopened browser panel ID")
return
}
// Simulate one delayed stale focus callback from the panel that was focused before reopen.
DispatchQueue.main.async {
workspace1.focusPanel(preReopenPanelId)
}
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace1.id)
XCTAssertEqual(workspace1.focusedPanelId, reopenedPanelId)
XCTAssertTrue(workspace1.panels[reopenedPanelId] is BrowserPanel)
}
func testReopenInSameWorkspaceWinsAgainstSingleDeferredStaleFocus() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let preReopenPanelId = workspace.focusedPanelId,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/stale-focus-same-ws")) else {
XCTFail("Expected initial workspace state and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(workspace.closePanel(closedBrowserId, force: true))
drainMainQueue()
let panelIdsBeforeReopen = Set(workspace.panels.keys)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
guard let reopenedPanelId = singleNewPanelId(in: workspace, comparedTo: panelIdsBeforeReopen) else {
XCTFail("Expected reopened browser panel ID")
return
}
// Simulate one delayed stale focus callback from the panel that was focused before reopen.
DispatchQueue.main.async {
workspace.focusPanel(preReopenPanelId)
}
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace.id)
XCTAssertEqual(workspace.focusedPanelId, reopenedPanelId)
XCTAssertTrue(workspace.panels[reopenedPanelId] is BrowserPanel)
}
private func isFocusedPanelBrowser(in workspace: Workspace) -> Bool {
guard let focusedPanelId = workspace.focusedPanelId else { return false }
return workspace.panels[focusedPanelId] is BrowserPanel
}
private func singleNewPanelId(in workspace: Workspace, comparedTo previousPanelIds: Set<UUID>) -> UUID? {
let newPanelIds = Set(workspace.panels.keys).subtracting(previousPanelIds)
guard newPanelIds.count == 1 else { return nil }
return newPanelIds.first
}
private func drainMainQueue() {
let expectation = expectation(description: "drain main queue")
DispatchQueue.main.async {
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
}