cmux/cmuxTests/SidebarOrderingTests.swift

975 lines
33 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
final class SidebarActiveForegroundColorTests: XCTestCase {
func testLightAppearanceUsesBlackWithRequestedOpacity() {
guard let lightAppearance = NSAppearance(named: .aqua),
let color = sidebarActiveForegroundNSColor(
opacity: 0.8,
appAppearance: lightAppearance
).usingColorSpace(.sRGB) else {
XCTFail("Expected sRGB-convertible color")
return
}
XCTAssertEqual(color.redComponent, 0, accuracy: 0.001)
XCTAssertEqual(color.greenComponent, 0, accuracy: 0.001)
XCTAssertEqual(color.blueComponent, 0, accuracy: 0.001)
XCTAssertEqual(color.alphaComponent, 0.8, accuracy: 0.001)
}
func testDarkAppearanceUsesWhiteWithRequestedOpacity() {
guard let darkAppearance = NSAppearance(named: .darkAqua),
let color = sidebarActiveForegroundNSColor(
opacity: 0.65,
appAppearance: darkAppearance
).usingColorSpace(.sRGB) else {
XCTFail("Expected sRGB-convertible color")
return
}
XCTAssertEqual(color.redComponent, 1, accuracy: 0.001)
XCTAssertEqual(color.greenComponent, 1, accuracy: 0.001)
XCTAssertEqual(color.blueComponent, 1, accuracy: 0.001)
XCTAssertEqual(color.alphaComponent, 0.65, accuracy: 0.001)
}
}
final class SidebarBranchLayoutSettingsTests: XCTestCase {
func testDefaultUsesVerticalLayout() {
let suiteName = "SidebarBranchLayoutSettingsTests.Default.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
XCTAssertTrue(SidebarBranchLayoutSettings.usesVerticalLayout(defaults: defaults))
}
func testStoredPreferenceOverridesDefault() {
let suiteName = "SidebarBranchLayoutSettingsTests.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(false, forKey: SidebarBranchLayoutSettings.key)
XCTAssertFalse(SidebarBranchLayoutSettings.usesVerticalLayout(defaults: defaults))
defaults.set(true, forKey: SidebarBranchLayoutSettings.key)
XCTAssertTrue(SidebarBranchLayoutSettings.usesVerticalLayout(defaults: defaults))
}
}
final class SidebarActiveTabIndicatorSettingsTests: XCTestCase {
func testDefaultStyleWhenUnset() {
let suiteName = "SidebarActiveTabIndicatorSettingsTests.Default.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.removeObject(forKey: SidebarActiveTabIndicatorSettings.styleKey)
XCTAssertEqual(
SidebarActiveTabIndicatorSettings.current(defaults: defaults),
SidebarActiveTabIndicatorSettings.defaultStyle
)
}
func testStoredStyleParsesAndInvalidFallsBack() {
let suiteName = "SidebarActiveTabIndicatorSettingsTests.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(SidebarActiveTabIndicatorStyle.leftRail.rawValue, forKey: SidebarActiveTabIndicatorSettings.styleKey)
XCTAssertEqual(SidebarActiveTabIndicatorSettings.current(defaults: defaults), .leftRail)
defaults.set("rail", forKey: SidebarActiveTabIndicatorSettings.styleKey)
XCTAssertEqual(SidebarActiveTabIndicatorSettings.current(defaults: defaults), .leftRail)
defaults.set("not-a-style", forKey: SidebarActiveTabIndicatorSettings.styleKey)
XCTAssertEqual(
SidebarActiveTabIndicatorSettings.current(defaults: defaults),
SidebarActiveTabIndicatorSettings.defaultStyle
)
}
}
final class SidebarRemoteErrorCopySupportTests: XCTestCase {
func testMenuLabelIsNilWhenThereAreNoErrors() {
XCTAssertNil(SidebarRemoteErrorCopySupport.menuLabel(for: []))
XCTAssertNil(SidebarRemoteErrorCopySupport.clipboardText(for: []))
}
func testSingleErrorUsesCopyErrorLabelAndSingleLinePayload() {
let entries = [
SidebarRemoteErrorCopyEntry(
workspaceTitle: "alpha",
target: "devbox:22",
detail: "failed to start reverse relay"
)
]
XCTAssertEqual(SidebarRemoteErrorCopySupport.menuLabel(for: entries), "Copy Error")
XCTAssertEqual(
SidebarRemoteErrorCopySupport.clipboardText(for: entries),
"SSH error (devbox:22): failed to start reverse relay"
)
}
func testMultipleErrorsUseCopyErrorsLabelAndEnumeratedPayload() {
let entries = [
SidebarRemoteErrorCopyEntry(
workspaceTitle: "alpha",
target: "devbox-a:22",
detail: "connection timed out"
),
SidebarRemoteErrorCopyEntry(
workspaceTitle: "beta",
target: "devbox-b:22",
detail: "permission denied"
),
]
XCTAssertEqual(SidebarRemoteErrorCopySupport.menuLabel(for: entries), "Copy Errors")
XCTAssertEqual(
SidebarRemoteErrorCopySupport.clipboardText(for: entries),
"""
1. alpha (devbox-a:22): connection timed out
2. beta (devbox-b:22): permission denied
"""
)
}
func testClipboardTextSingleEntryUsesStructuredEntryFields() {
let entry = SidebarRemoteErrorCopyEntry(
workspaceTitle: "alpha",
target: "devbox:22",
detail: "failed to bootstrap daemon"
)
XCTAssertEqual(
SidebarRemoteErrorCopySupport.clipboardText(for: [entry]),
"SSH error (devbox:22): failed to bootstrap daemon"
)
}
}
final class SidebarBranchOrderingTests: XCTestCase {
func testOrderedUniqueBranchesDedupesByNameAndMergesDirtyState() {
let first = UUID()
let second = UUID()
let third = UUID()
let branches = SidebarBranchOrdering.orderedUniqueBranches(
orderedPanelIds: [first, second, third],
panelBranches: [
first: SidebarGitBranchState(branch: "main", isDirty: false),
second: SidebarGitBranchState(branch: "feature", isDirty: false),
third: SidebarGitBranchState(branch: "main", isDirty: true)
],
fallbackBranch: SidebarGitBranchState(branch: "fallback", isDirty: false)
)
XCTAssertEqual(
branches,
[
SidebarBranchOrdering.BranchEntry(name: "main", isDirty: true),
SidebarBranchOrdering.BranchEntry(name: "feature", isDirty: false)
]
)
}
func testOrderedUniqueBranchesUsesFallbackWhenNoPanelBranchesExist() {
let branches = SidebarBranchOrdering.orderedUniqueBranches(
orderedPanelIds: [],
panelBranches: [:],
fallbackBranch: SidebarGitBranchState(branch: "fallback", isDirty: true)
)
XCTAssertEqual(
branches,
[SidebarBranchOrdering.BranchEntry(name: "fallback", isDirty: true)]
)
}
func testOrderedUniqueBranchDirectoryEntriesDedupesPairsAndMergesDirtyState() {
let first = UUID()
let second = UUID()
let third = UUID()
let fourth = UUID()
let fifth = UUID()
let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries(
orderedPanelIds: [first, second, third, fourth, fifth],
panelBranches: [
first: SidebarGitBranchState(branch: "main", isDirty: false),
second: SidebarGitBranchState(branch: "feature", isDirty: false),
third: SidebarGitBranchState(branch: "main", isDirty: true),
fourth: SidebarGitBranchState(branch: "main", isDirty: false)
],
panelDirectories: [
first: "/repo/a",
second: "/repo/b",
third: "/repo/a",
fourth: "/repo/d",
fifth: "/repo/e"
],
defaultDirectory: "/repo/default",
homeDirectoryForTildeExpansion: nil,
fallbackBranch: SidebarGitBranchState(branch: "fallback", isDirty: false)
)
XCTAssertEqual(
rows,
[
SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: true, directory: "/repo/a"),
SidebarBranchOrdering.BranchDirectoryEntry(branch: "feature", isDirty: false, directory: "/repo/b"),
SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: false, directory: "/repo/d"),
SidebarBranchOrdering.BranchDirectoryEntry(branch: nil, isDirty: false, directory: "/repo/e")
]
)
}
func testOrderedUniqueBranchDirectoryEntriesUsesFallbackBranchWhenPanelBranchesMissing() {
let first = UUID()
let second = UUID()
let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries(
orderedPanelIds: [first, second],
panelBranches: [:],
panelDirectories: [
first: "/repo/one",
second: "/repo/two"
],
defaultDirectory: "/repo/default",
homeDirectoryForTildeExpansion: nil,
fallbackBranch: SidebarGitBranchState(branch: "main", isDirty: true)
)
XCTAssertEqual(
rows,
[
SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: true, directory: "/repo/one"),
SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: true, directory: "/repo/two")
]
)
}
func testOrderedUniqueBranchDirectoryEntriesFallsBackWhenNoPanelsExist() {
let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries(
orderedPanelIds: [],
panelBranches: [:],
panelDirectories: [:],
defaultDirectory: "/repo/default",
homeDirectoryForTildeExpansion: nil,
fallbackBranch: SidebarGitBranchState(branch: "main", isDirty: false)
)
XCTAssertEqual(
rows,
[SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: false, directory: "/repo/default")]
)
}
func testOrderedUniqueBranchDirectoryEntriesKeepsAbsoluteDirectoryWhenLaterEntryUsesTildeAlias() {
let first = UUID()
let second = UUID()
let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries(
orderedPanelIds: [first, second],
panelBranches: [
first: SidebarGitBranchState(branch: "main", isDirty: false),
second: SidebarGitBranchState(branch: "feature", isDirty: true)
],
panelDirectories: [
first: "/home/remoteuser/project",
second: "~/project"
],
defaultDirectory: nil,
homeDirectoryForTildeExpansion: "/home/remoteuser",
fallbackBranch: nil
)
XCTAssertEqual(
rows,
[
SidebarBranchOrdering.BranchDirectoryEntry(
branch: "feature",
isDirty: true,
directory: "/home/remoteuser/project"
)
]
)
}
func testOrderedUniquePullRequestsFollowsPanelOrderAcrossSplitsAndTabs() {
let first = UUID()
let second = UUID()
let third = UUID()
let fourth = UUID()
let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests(
orderedPanelIds: [first, second, third, fourth],
panelPullRequests: [
first: pullRequestState(
number: 337,
label: "PR",
url: "https://github.com/manaflow-ai/cmux/pull/337",
status: .open
),
second: pullRequestState(
number: 18,
label: "MR",
url: "https://gitlab.com/manaflow/cmux/-/merge_requests/18",
status: .open
),
third: pullRequestState(
number: 337,
label: "PR",
url: "https://github.com/manaflow-ai/cmux/pull/337",
status: .merged
),
fourth: pullRequestState(
number: 92,
label: "PR",
url: "https://bitbucket.org/manaflow/cmux/pull-requests/92",
status: .closed
)
],
fallbackPullRequest: pullRequestState(
number: 1,
label: "PR",
url: "https://example.invalid/fallback/1",
status: .open
)
)
XCTAssertEqual(
pullRequests.map { "\($0.label)#\($0.number)" },
["PR#337", "MR#18", "PR#92"]
)
XCTAssertEqual(
pullRequests.map(\.status),
[.merged, .open, .closed]
)
}
func testOrderedUniquePullRequestsTreatsSameNumberDifferentLabelsAsDistinct() {
let first = UUID()
let second = UUID()
let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests(
orderedPanelIds: [first, second],
panelPullRequests: [
first: pullRequestState(
number: 42,
label: "PR",
url: "https://github.com/manaflow-ai/cmux/pull/42",
status: .open
),
second: pullRequestState(
number: 42,
label: "MR",
url: "https://gitlab.com/manaflow/cmux/-/merge_requests/42",
status: .open
)
],
fallbackPullRequest: nil
)
XCTAssertEqual(
pullRequests.map { "\($0.label)#\($0.number)" },
["PR#42", "MR#42"]
)
}
func testOrderedUniquePullRequestsTreatsSameNumberAndLabelDifferentUrlsAsDistinct() {
let first = UUID()
let second = UUID()
let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests(
orderedPanelIds: [first, second],
panelPullRequests: [
first: pullRequestState(
number: 42,
label: "PR",
url: "https://github.com/manaflow-ai/cmux/pull/42",
status: .open
),
second: pullRequestState(
number: 42,
label: "PR",
url: "https://github.com/manaflow-ai/other-repo/pull/42",
status: .open
)
],
fallbackPullRequest: nil
)
XCTAssertEqual(
pullRequests.map(\.url.absoluteString),
[
"https://github.com/manaflow-ai/cmux/pull/42",
"https://github.com/manaflow-ai/other-repo/pull/42"
]
)
}
func testOrderedUniquePullRequestsPrefersEntryWithChecksWhenStatusesMatch() {
let first = UUID()
let second = UUID()
let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests(
orderedPanelIds: [first, second],
panelPullRequests: [
first: pullRequestState(
number: 42,
label: "PR",
url: "https://github.com/manaflow-ai/cmux/pull/42",
status: .open
),
second: pullRequestState(
number: 42,
label: "PR",
url: "https://github.com/manaflow-ai/cmux/pull/42",
status: .open,
checks: .pass
)
],
fallbackPullRequest: nil
)
XCTAssertEqual(pullRequests.count, 1)
XCTAssertEqual(pullRequests.first?.checks, .pass)
}
@MainActor
func testUpdatePanelPullRequestPreservesExistingChecksWhenUpdateOmitsThem() {
let workspace = Workspace(title: "Tests", workingDirectory: FileManager.default.currentDirectoryPath, portOrdinal: 0)
guard let panelId = workspace.focusedPanelId else {
XCTFail("Expected focused panel for new workspace")
return
}
workspace.updatePanelPullRequest(
panelId: panelId,
number: 42,
label: "PR",
url: URL(string: "https://github.com/manaflow-ai/cmux/pull/42")!,
status: .open,
checks: .pass
)
workspace.updatePanelPullRequest(
panelId: panelId,
number: 42,
label: "PR",
url: URL(string: "https://github.com/manaflow-ai/cmux/pull/42")!,
status: .open
)
XCTAssertEqual(workspace.panelPullRequests[panelId]?.checks, .pass)
XCTAssertEqual(workspace.pullRequest?.checks, .pass)
}
func testOrderedUniquePullRequestsUsesFallbackWhenNoPanelPullRequestsExist() {
let fallback = pullRequestState(
number: 11,
label: "PR",
url: "https://github.com/manaflow-ai/cmux/pull/11",
status: .open
)
let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests(
orderedPanelIds: [],
panelPullRequests: [:],
fallbackPullRequest: fallback
)
XCTAssertEqual(pullRequests, [fallback])
}
@MainActor
func testUpdatePanelGitBranchClearsFocusedPullRequestWhenBranchChanges() {
let workspace = Workspace(title: "Tests", workingDirectory: FileManager.default.currentDirectoryPath, portOrdinal: 0)
guard let panelId = workspace.focusedPanelId else {
XCTFail("Expected focused panel for new workspace")
return
}
workspace.updatePanelGitBranch(panelId: panelId, branch: "feature/sidebar-pr", isDirty: false)
workspace.updatePanelPullRequest(
panelId: panelId,
number: 1629,
label: "PR",
url: URL(string: "https://github.com/manaflow-ai/cmux/pull/1629")!,
status: .open
)
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
XCTAssertNil(workspace.pullRequest)
XCTAssertNil(workspace.panelPullRequests[panelId])
XCTAssertTrue(workspace.sidebarPullRequestsInDisplayOrder().isEmpty)
}
@MainActor
func testSidebarPullRequestsHideBranchMismatches() {
let workspace = Workspace(title: "Tests", workingDirectory: FileManager.default.currentDirectoryPath, portOrdinal: 0)
guard let panelId = workspace.focusedPanelId else {
XCTFail("Expected focused panel for new workspace")
return
}
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
workspace.updatePanelPullRequest(
panelId: panelId,
number: 1629,
label: "PR",
url: URL(string: "https://github.com/manaflow-ai/cmux/pull/1629")!,
status: .open,
branch: "feature/sidebar-pr"
)
XCTAssertTrue(workspace.sidebarPullRequestsInDisplayOrder().isEmpty)
}
private func pullRequestState(
number: Int,
label: String,
url: String,
status: SidebarPullRequestStatus,
branch: String? = nil,
checks: SidebarPullRequestChecksStatus? = nil
) -> SidebarPullRequestState {
SidebarPullRequestState(
number: number,
label: label,
url: URL(string: url)!,
status: status,
branch: branch,
checks: checks
)
}
}
final class SidebarDropPlannerTests: XCTestCase {
func testNoIndicatorForNoOpEdges() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
XCTAssertNil(
SidebarDropPlanner.indicator(
draggedTabId: first,
targetTabId: first,
tabIds: tabIds,
pinnedTabIds: []
)
)
XCTAssertNil(
SidebarDropPlanner.indicator(
draggedTabId: third,
targetTabId: nil,
tabIds: tabIds,
pinnedTabIds: []
)
)
}
func testNoIndicatorWhenOnlyOneTabExists() {
let only = UUID()
XCTAssertNil(
SidebarDropPlanner.indicator(
draggedTabId: only,
targetTabId: nil,
tabIds: [only],
pinnedTabIds: []
)
)
XCTAssertNil(
SidebarDropPlanner.indicator(
draggedTabId: only,
targetTabId: only,
tabIds: [only],
pinnedTabIds: []
)
)
}
func testIndicatorAppearsForRealMoveToEnd() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
let indicator = SidebarDropPlanner.indicator(
draggedTabId: second,
targetTabId: nil,
tabIds: tabIds,
pinnedTabIds: []
)
XCTAssertEqual(indicator?.tabId, nil)
XCTAssertEqual(indicator?.edge, .bottom)
}
func testTargetIndexForMoveToEndFromMiddle() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
let index = SidebarDropPlanner.targetIndex(
draggedTabId: second,
targetTabId: nil,
indicator: SidebarDropIndicator(tabId: nil, edge: .bottom),
tabIds: tabIds,
pinnedTabIds: []
)
XCTAssertEqual(index, 2)
}
func testNoIndicatorForSelfDropInMiddle() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
XCTAssertNil(
SidebarDropPlanner.indicator(
draggedTabId: second,
targetTabId: second,
tabIds: tabIds,
pinnedTabIds: []
)
)
}
func testPointerEdgeTopCanSuppressNoOpWhenDraggingFirstOverSecond() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
XCTAssertNil(
SidebarDropPlanner.indicator(
draggedTabId: first,
targetTabId: second,
tabIds: tabIds,
pinnedTabIds: [],
pointerY: 2,
targetHeight: 40
)
)
}
func testPointerEdgeBottomAllowsMoveWhenDraggingFirstOverSecond() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
let indicator = SidebarDropPlanner.indicator(
draggedTabId: first,
targetTabId: second,
tabIds: tabIds,
pinnedTabIds: [],
pointerY: 38,
targetHeight: 40
)
XCTAssertEqual(indicator?.tabId, third)
XCTAssertEqual(indicator?.edge, .top)
XCTAssertEqual(
SidebarDropPlanner.targetIndex(
draggedTabId: first,
targetTabId: second,
indicator: indicator,
tabIds: tabIds,
pinnedTabIds: []
),
1
)
}
func testEquivalentBoundaryInputsResolveToSingleCanonicalIndicator() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
let fromBottomOfFirst = SidebarDropPlanner.indicator(
draggedTabId: third,
targetTabId: first,
tabIds: tabIds,
pinnedTabIds: [],
pointerY: 38,
targetHeight: 40
)
let fromTopOfSecond = SidebarDropPlanner.indicator(
draggedTabId: third,
targetTabId: second,
tabIds: tabIds,
pinnedTabIds: [],
pointerY: 2,
targetHeight: 40
)
XCTAssertEqual(fromBottomOfFirst?.tabId, second)
XCTAssertEqual(fromBottomOfFirst?.edge, .top)
XCTAssertEqual(fromTopOfSecond?.tabId, second)
XCTAssertEqual(fromTopOfSecond?.edge, .top)
}
func testPointerEdgeBottomSuppressesNoOpWhenDraggingLastOverSecond() {
let first = UUID()
let second = UUID()
let third = UUID()
let tabIds = [first, second, third]
XCTAssertNil(
SidebarDropPlanner.indicator(
draggedTabId: third,
targetTabId: second,
tabIds: tabIds,
pinnedTabIds: [],
pointerY: 38,
targetHeight: 40
)
)
}
func testIndicatorSnapsUnpinnedDropToFirstUnpinnedBoundaryWhenHoveringPinnedWorkspace() {
let pinnedA = UUID()
let pinnedB = UUID()
let unpinnedA = UUID()
let unpinnedB = UUID()
let tabIds = [pinnedA, pinnedB, unpinnedA, unpinnedB]
let pinnedIds: Set<UUID> = [pinnedA, pinnedB]
let indicator = SidebarDropPlanner.indicator(
draggedTabId: unpinnedB,
targetTabId: pinnedA,
tabIds: tabIds,
pinnedTabIds: pinnedIds,
pointerY: 2,
targetHeight: 40
)
XCTAssertEqual(indicator?.tabId, unpinnedA)
XCTAssertEqual(indicator?.edge, .top)
}
func testTargetIndexSnapsUnpinnedDropToFirstUnpinnedBoundaryWhenHoveringPinnedWorkspace() {
let pinnedA = UUID()
let pinnedB = UUID()
let unpinnedA = UUID()
let unpinnedB = UUID()
let tabIds = [pinnedA, pinnedB, unpinnedA, unpinnedB]
let pinnedIds: Set<UUID> = [pinnedA, pinnedB]
let targetIndex = SidebarDropPlanner.targetIndex(
draggedTabId: unpinnedB,
targetTabId: pinnedA,
indicator: SidebarDropIndicator(tabId: pinnedA, edge: .top),
tabIds: tabIds,
pinnedTabIds: pinnedIds
)
XCTAssertEqual(targetIndex, 2)
}
}
final class SidebarDragAutoScrollPlannerTests: XCTestCase {
func testAutoScrollPlanTriggersNearTopAndBottomOnly() {
let topPlan = SidebarDragAutoScrollPlanner.plan(distanceToTop: 4, distanceToBottom: 96, edgeInset: 44, minStep: 2, maxStep: 12)
XCTAssertEqual(topPlan?.direction, .up)
XCTAssertNotNil(topPlan)
let bottomPlan = SidebarDragAutoScrollPlanner.plan(distanceToTop: 96, distanceToBottom: 4, edgeInset: 44, minStep: 2, maxStep: 12)
XCTAssertEqual(bottomPlan?.direction, .down)
XCTAssertNotNil(bottomPlan)
XCTAssertNil(
SidebarDragAutoScrollPlanner.plan(distanceToTop: 60, distanceToBottom: 60, edgeInset: 44, minStep: 2, maxStep: 12)
)
}
func testAutoScrollPlanSpeedsUpCloserToEdge() {
let nearTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: 1, distanceToBottom: 99, edgeInset: 44, minStep: 2, maxStep: 12)
let midTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: 22, distanceToBottom: 78, edgeInset: 44, minStep: 2, maxStep: 12)
XCTAssertNotNil(nearTop)
XCTAssertNotNil(midTop)
XCTAssertGreaterThan(nearTop?.pointsPerTick ?? 0, midTop?.pointsPerTick ?? 0)
}
func testAutoScrollPlanStillTriggersWhenPointerIsPastEdge() {
let aboveTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: -500, distanceToBottom: 600, edgeInset: 44, minStep: 2, maxStep: 12)
XCTAssertEqual(aboveTop?.direction, .up)
XCTAssertEqual(aboveTop?.pointsPerTick, 12)
let belowBottom = SidebarDragAutoScrollPlanner.plan(distanceToTop: 600, distanceToBottom: -500, edgeInset: 44, minStep: 2, maxStep: 12)
XCTAssertEqual(belowBottom?.direction, .down)
XCTAssertEqual(belowBottom?.pointsPerTick, 12)
}
}
final class TerminalControllerSidebarDedupeTests: XCTestCase {
func testShouldReplaceStatusEntryReturnsFalseForUnchangedPayload() {
let current = SidebarStatusEntry(
key: "agent",
value: "idle",
icon: "bolt",
color: "#ffffff",
timestamp: Date(timeIntervalSince1970: 123)
)
XCTAssertFalse(
TerminalController.shouldReplaceStatusEntry(
current: current,
key: "agent",
value: "idle",
icon: "bolt",
color: "#ffffff",
url: nil,
priority: 0,
format: .plain
)
)
}
func testShouldReplaceStatusEntryReturnsTrueWhenValueChanges() {
let current = SidebarStatusEntry(
key: "agent",
value: "idle",
icon: "bolt",
color: "#ffffff",
timestamp: Date(timeIntervalSince1970: 123)
)
XCTAssertTrue(
TerminalController.shouldReplaceStatusEntry(
current: current,
key: "agent",
value: "running",
icon: "bolt",
color: "#ffffff",
url: nil,
priority: 0,
format: .plain
)
)
}
func testShouldReplaceProgressReturnsFalseForUnchangedPayload() {
XCTAssertFalse(
TerminalController.shouldReplaceProgress(
current: SidebarProgressState(value: 0.42, label: "indexing"),
value: 0.42,
label: "indexing"
)
)
}
func testShouldReplaceGitBranchReturnsFalseForUnchangedPayload() {
XCTAssertFalse(
TerminalController.shouldReplaceGitBranch(
current: SidebarGitBranchState(branch: "main", isDirty: true),
branch: "main",
isDirty: true
)
)
}
func testShouldReplacePortsIgnoresOrderAndDuplicates() {
XCTAssertFalse(
TerminalController.shouldReplacePorts(
current: [9229, 3000],
next: [3000, 9229, 3000]
)
)
XCTAssertTrue(
TerminalController.shouldReplacePorts(
current: [9229, 3000],
next: [3000]
)
)
}
func testExplicitSocketScopeParsesValidUUIDTabAndPanel() {
let workspaceId = UUID()
let panelId = UUID()
let scope = TerminalController.explicitSocketScope(
options: [
"tab": workspaceId.uuidString,
"panel": panelId.uuidString
]
)
XCTAssertEqual(scope?.workspaceId, workspaceId)
XCTAssertEqual(scope?.panelId, panelId)
}
func testExplicitSocketScopeAcceptsSurfaceAlias() {
let workspaceId = UUID()
let panelId = UUID()
let scope = TerminalController.explicitSocketScope(
options: [
"tab": workspaceId.uuidString,
"surface": panelId.uuidString
]
)
XCTAssertEqual(scope?.workspaceId, workspaceId)
XCTAssertEqual(scope?.panelId, panelId)
}
func testExplicitSocketScopeRejectsMissingOrInvalidValues() {
XCTAssertNil(TerminalController.explicitSocketScope(options: [:]))
XCTAssertNil(TerminalController.explicitSocketScope(options: ["tab": "workspace:1", "panel": UUID().uuidString]))
XCTAssertNil(TerminalController.explicitSocketScope(options: ["tab": UUID().uuidString, "panel": "surface:1"]))
}
func testNormalizeReportedDirectoryTrimsWhitespace() {
XCTAssertEqual(
TerminalController.normalizeReportedDirectory(" /Users/cmux/project "),
"/Users/cmux/project"
)
}
func testNormalizeReportedDirectoryResolvesFileURL() {
XCTAssertEqual(
TerminalController.normalizeReportedDirectory("file:///Users/cmux/project"),
"/Users/cmux/project"
)
}
func testNormalizeReportedDirectoryLeavesInvalidURLTrimmed() {
XCTAssertEqual(
TerminalController.normalizeReportedDirectory(" file://bad host "),
"file://bad host"
)
}
}