Split 16k-line mega test file, bump CI timeout, stream xcodebuild output (#1717)

CmuxWebViewKeyEquivalentTests.swift grew to 15,907 lines with 100+ test classes.
Swift compiles per-file, so this single file serialized all type-checking onto one
compiler process, pushing CI past the 20-minute timeout after core-file changes.

Split into 10 domain-based files (1k-3k lines each) so Xcode can compile them in
parallel. Also bump timeout-minutes from 20 to 30 for headroom, stream xcodebuild
output via tee instead of capturing to a variable (makes CI logs debuggable), and
add 5 test files that were missing from the pbxproj Sources build phase.

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
Lawrence Chen 2026-03-18 01:17:25 -07:00 committed by GitHub
parent 33d21ea19e
commit ac83af62ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 16227 additions and 15919 deletions

View file

@ -15,11 +15,11 @@ jobs:
matrix:
include:
- os: warp-macos-15-arm64-6x
timeout: 20
timeout: 30
smoke: true
skip_zig: false
- os: warp-macos-26-arm64-6x
timeout: 20
timeout: 30
smoke: false
skip_zig: true # zig 0.15.2 MachO linker can't resolve libSystem on macOS 26
runs-on: ${{ matrix.os }}
@ -133,8 +133,9 @@ jobs:
}
set +e
OUTPUT=$(run_unit_tests)
EXIT_CODE=$?
run_unit_tests | tee /tmp/test-output.txt
EXIT_CODE=${PIPESTATUS[0]}
OUTPUT=$(cat /tmp/test-output.txt)
set -e
# SwiftPM binary artifact resolution can occasionally fail on ephemeral
@ -145,12 +146,12 @@ jobs:
mkdir -p ~/Library/Caches/org.swift.swiftpm
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
set +e
OUTPUT=$(run_unit_tests)
EXIT_CODE=$?
run_unit_tests | tee /tmp/test-output.txt
EXIT_CODE=${PIPESTATUS[0]}
OUTPUT=$(cat /tmp/test-output.txt)
set -e
fi
echo "$OUTPUT"
if [ "$EXIT_CODE" -ne 0 ]; then
SUMMARY=$(echo "$OUTPUT" | grep "Executed.*tests.*with.*failures" | tail -1)
if echo "$SUMMARY" | grep -q "(0 unexpected)"; then

View file

@ -86,7 +86,6 @@
D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */; };
FB100000A1B2C3D4E5F60718 /* BrowserImportProfilesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB100001A1B2C3D4E5F60718 /* BrowserImportProfilesUITests.swift */; };
E1000000A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */; };
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */; };
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */; };
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; };
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */; };
@ -103,6 +102,21 @@
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; };
E12E88F82733EC42F32C36A3 /* BrowserConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */; };
1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */; };
46F6AC15863EC84DCD3770A2 /* TerminalAndGhosttyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */; };
6B524A0BA34FD46A771335AB /* WorkspaceUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F8ED91A4B55D34BE6A0668 /* WorkspaceUnitTests.swift */; };
063BC42CEE257D6213A2E30C /* WindowAndDragTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */; };
1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */; };
CB23911D7E131E8FBC9B82B6 /* SidebarOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC39DE4B96D1931C52AF7D68 /* SidebarOrderingTests.swift */; };
4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */; };
734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */; };
B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */; };
DCC935C5F55C1DCB33E25521 /* WorkspacePullRequestSidebarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14A7DC53B9CA33BE2A421711 /* WorkspacePullRequestSidebarTests.swift */; };
0F2C25F9170130F8DC09DD1B /* WorkspaceManualUnreadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */; };
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */; };
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */; };
2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -236,7 +250,6 @@
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarSuggestionsUITests.swift; sourceTree = "<group>"; };
FB100001A1B2C3D4E5F60718 /* BrowserImportProfilesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserImportProfilesUITests.swift; sourceTree = "<group>"; };
E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = "<group>"; };
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWebViewKeyEquivalentTests.swift; sourceTree = "<group>"; };
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillReleaseVisibilityTests.swift; sourceTree = "<group>"; };
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEInputTests.swift; sourceTree = "<group>"; };
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfigTests.swift; sourceTree = "<group>"; };
@ -253,6 +266,21 @@
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = "<group>"; };
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserConfigTests.swift; sourceTree = "<group>"; };
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPanelTests.swift; sourceTree = "<group>"; };
02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalAndGhosttyTests.swift; sourceTree = "<group>"; };
71F8ED91A4B55D34BE6A0668 /* WorkspaceUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceUnitTests.swift; sourceTree = "<group>"; };
BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAndDragTests.swift; sourceTree = "<group>"; };
6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutAndCommandPaletteTests.swift; sourceTree = "<group>"; };
BC39DE4B96D1931C52AF7D68 /* SidebarOrderingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOrderingTests.swift; sourceTree = "<group>"; };
B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnibarAndToolsTests.swift; sourceTree = "<group>"; };
D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAndMenuBarTests.swift; sourceTree = "<group>"; };
42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerUnitTests.swift; sourceTree = "<group>"; };
14A7DC53B9CA33BE2A421711 /* WorkspacePullRequestSidebarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacePullRequestSidebarTests.swift; sourceTree = "<group>"; };
1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceManualUnreadTests.swift; sourceTree = "<group>"; };
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWidthPolicyTests.swift; sourceTree = "<group>"; };
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalControllerSocketSecurityTests.swift; sourceTree = "<group>"; };
10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerSessionSnapshotTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -475,7 +503,6 @@
F1000003A1B2C3D4E5F60718 /* cmuxTests */ = {
isa = PBXGroup;
children = (
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */,
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */,
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */,
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
@ -489,6 +516,21 @@
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */,
A5008380 /* BrowserFindJavaScriptTests.swift */,
A5008382 /* CommandPaletteSearchEngineTests.swift */,
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */,
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */,
02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */,
71F8ED91A4B55D34BE6A0668 /* WorkspaceUnitTests.swift */,
BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */,
6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */,
BC39DE4B96D1931C52AF7D68 /* SidebarOrderingTests.swift */,
B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */,
D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */,
42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */,
14A7DC53B9CA33BE2A421711 /* WorkspacePullRequestSidebarTests.swift */,
1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */,
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */,
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */,
10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */,
);
path = cmuxTests;
sourceTree = "<group>";
@ -719,7 +761,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */,
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */,
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,832 @@
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
final class NotificationDockBadgeTests: XCTestCase {
private final class NotificationSettingsAlertSpy: NSAlert {
private(set) var beginSheetModalCallCount = 0
private(set) var runModalCallCount = 0
var nextResponse: NSApplication.ModalResponse = .alertFirstButtonReturn
override func beginSheetModal(
for sheetWindow: NSWindow,
completionHandler handler: ((NSApplication.ModalResponse) -> Void)?
) {
beginSheetModalCallCount += 1
handler?(nextResponse)
}
override func runModal() -> NSApplication.ModalResponse {
runModalCallCount += 1
return nextResponse
}
}
override func tearDown() {
TerminalNotificationStore.shared.resetNotificationSettingsPromptHooksForTesting()
TerminalNotificationStore.shared.replaceNotificationsForTesting([])
super.tearDown()
}
func testDockBadgeLabelEnabledAndCounted() {
XCTAssertEqual(TerminalNotificationStore.dockBadgeLabel(unreadCount: 1, isEnabled: true), "1")
XCTAssertEqual(TerminalNotificationStore.dockBadgeLabel(unreadCount: 42, isEnabled: true), "42")
XCTAssertEqual(TerminalNotificationStore.dockBadgeLabel(unreadCount: 100, isEnabled: true), "99+")
}
func testDockBadgeLabelHiddenWhenDisabledOrZero() {
XCTAssertNil(TerminalNotificationStore.dockBadgeLabel(unreadCount: 0, isEnabled: true))
XCTAssertNil(TerminalNotificationStore.dockBadgeLabel(unreadCount: 5, isEnabled: false))
}
func testDockBadgeLabelShowsRunTagEvenWithoutUnread() {
XCTAssertEqual(
TerminalNotificationStore.dockBadgeLabel(unreadCount: 0, isEnabled: true, runTag: "verify-tag"),
"verify-tag"
)
}
func testDockBadgeLabelCombinesRunTagAndUnreadCount() {
XCTAssertEqual(
TerminalNotificationStore.dockBadgeLabel(unreadCount: 7, isEnabled: true, runTag: "verify"),
"verify:7"
)
XCTAssertEqual(
TerminalNotificationStore.dockBadgeLabel(unreadCount: 120, isEnabled: true, runTag: "verify"),
"verify:99+"
)
}
func testNotificationBadgePreferenceDefaultsToEnabled() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
XCTAssertTrue(NotificationBadgeSettings.isDockBadgeEnabled(defaults: defaults))
defaults.set(false, forKey: NotificationBadgeSettings.dockBadgeEnabledKey)
XCTAssertFalse(NotificationBadgeSettings.isDockBadgeEnabled(defaults: defaults))
defaults.set(true, forKey: NotificationBadgeSettings.dockBadgeEnabledKey)
XCTAssertTrue(NotificationBadgeSettings.isDockBadgeEnabled(defaults: defaults))
}
func testNotificationPaneFlashPreferenceDefaultsToEnabled() {
let suiteName = "NotificationPaneFlashSettingsTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
XCTAssertTrue(NotificationPaneFlashSettings.isEnabled(defaults: defaults))
defaults.set(false, forKey: NotificationPaneFlashSettings.enabledKey)
XCTAssertFalse(NotificationPaneFlashSettings.isEnabled(defaults: defaults))
defaults.set(true, forKey: NotificationPaneFlashSettings.enabledKey)
XCTAssertTrue(NotificationPaneFlashSettings.isEnabled(defaults: defaults))
}
func testMenuBarExtraPreferenceDefaultsToVisible() {
let suiteName = "MenuBarExtraVisibilityTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
XCTAssertTrue(MenuBarExtraSettings.showsMenuBarExtra(defaults: defaults))
defaults.set(false, forKey: MenuBarExtraSettings.showInMenuBarKey)
XCTAssertFalse(MenuBarExtraSettings.showsMenuBarExtra(defaults: defaults))
defaults.set(true, forKey: MenuBarExtraSettings.showInMenuBarKey)
XCTAssertTrue(MenuBarExtraSettings.showsMenuBarExtra(defaults: defaults))
}
func testNotificationSoundUsesSystemSoundForDefaultAndNamedSounds() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
XCTAssertTrue(NotificationSoundSettings.usesSystemSound(defaults: defaults))
defaults.set("Ping", forKey: NotificationSoundSettings.key)
XCTAssertTrue(NotificationSoundSettings.usesSystemSound(defaults: defaults))
XCTAssertNotNil(NotificationSoundSettings.sound(defaults: defaults))
}
func testNotificationSoundDisablesSystemSoundForNoneAndCustomFile() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
defaults.set("none", forKey: NotificationSoundSettings.key)
XCTAssertFalse(NotificationSoundSettings.usesSystemSound(defaults: defaults))
XCTAssertNil(NotificationSoundSettings.sound(defaults: defaults))
defaults.set(NotificationSoundSettings.customFileValue, forKey: NotificationSoundSettings.key)
XCTAssertFalse(NotificationSoundSettings.usesSystemSound(defaults: defaults))
XCTAssertNil(NotificationSoundSettings.sound(defaults: defaults))
}
func testNotificationCustomFileURLExpandsTildePath() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let rawPath = "~/Library/Sounds/my-custom.wav"
defaults.set(rawPath, forKey: NotificationSoundSettings.customFilePathKey)
let expectedPath = (rawPath as NSString).expandingTildeInPath
XCTAssertEqual(NotificationSoundSettings.customFileURL(defaults: defaults)?.path, expectedPath)
}
func testNotificationCustomFileSelectionMustBeExplicit() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
defaults.set("~/Library/Sounds/my-custom.wav", forKey: NotificationSoundSettings.customFilePathKey)
defaults.set("none", forKey: NotificationSoundSettings.key)
XCTAssertFalse(NotificationSoundSettings.isCustomFileSelected(defaults: defaults))
defaults.set("Ping", forKey: NotificationSoundSettings.key)
XCTAssertFalse(NotificationSoundSettings.isCustomFileSelected(defaults: defaults))
defaults.set(NotificationSoundSettings.customFileValue, forKey: NotificationSoundSettings.key)
XCTAssertTrue(NotificationSoundSettings.isCustomFileSelected(defaults: defaults))
}
func testNotificationCustomStagingPreservesSourceFileWithCmuxPrefix() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let fileManager = FileManager.default
let soundsDirectory = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Sounds", isDirectory: true)
do {
try fileManager.createDirectory(at: soundsDirectory, withIntermediateDirectories: true)
} catch {
XCTFail("Failed to create sounds directory: \(error)")
return
}
let sourceURL = soundsDirectory.appendingPathComponent(
"cmux-custom-notification-sound.source-\(UUID().uuidString).wav",
isDirectory: false
)
defer {
try? fileManager.removeItem(at: sourceURL)
}
do {
try Data("test".utf8).write(to: sourceURL, options: .atomic)
} catch {
XCTFail("Failed to write source custom sound file: \(error)")
return
}
defaults.set(NotificationSoundSettings.customFileValue, forKey: NotificationSoundSettings.key)
defaults.set(sourceURL.path, forKey: NotificationSoundSettings.customFilePathKey)
_ = NotificationSoundSettings.sound(defaults: defaults)
guard let stagedName = NotificationSoundSettings.stagedCustomSoundName(defaults: defaults) else {
XCTFail("Expected staged custom sound name")
return
}
let stagedURL = soundsDirectory.appendingPathComponent(stagedName, isDirectory: false)
defer {
try? fileManager.removeItem(at: stagedURL)
}
XCTAssertTrue(fileManager.fileExists(atPath: sourceURL.path))
XCTAssertTrue(fileManager.fileExists(atPath: stagedURL.path))
XCTAssertTrue(stagedName.hasPrefix("cmux-custom-notification-sound-"))
XCTAssertTrue(stagedName.hasSuffix(".wav"))
}
func testNotificationCustomUnsupportedExtensionsStageAsCaf() {
XCTAssertEqual(
NotificationSoundSettings.stagedCustomSoundFileExtension(forSourceExtension: "mp3"),
"caf"
)
XCTAssertEqual(
NotificationSoundSettings.stagedCustomSoundFileExtension(forSourceExtension: "M4A"),
"caf"
)
XCTAssertEqual(
NotificationSoundSettings.stagedCustomSoundFileExtension(forSourceExtension: "wav"),
"wav"
)
XCTAssertEqual(
NotificationSoundSettings.stagedCustomSoundFileExtension(forSourceExtension: "AIFF"),
"aiff"
)
let sourceA = URL(fileURLWithPath: "/tmp/custom-a.mp3")
let sourceB = URL(fileURLWithPath: "/tmp/custom-b.mp3")
let stagedA = NotificationSoundSettings.stagedCustomSoundFileName(
forSourceURL: sourceA,
destinationExtension: "caf"
)
let stagedB = NotificationSoundSettings.stagedCustomSoundFileName(
forSourceURL: sourceB,
destinationExtension: "caf"
)
XCTAssertNotEqual(stagedA, stagedB)
XCTAssertTrue(stagedA.hasPrefix("cmux-custom-notification-sound-"))
XCTAssertTrue(stagedA.hasSuffix(".caf"))
}
func testNotificationCustomPreparationKeepsActiveSourceMetadataSidecar() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let fileManager = FileManager.default
let soundsDirectory = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Sounds", isDirectory: true)
do {
try fileManager.createDirectory(at: soundsDirectory, withIntermediateDirectories: true)
} catch {
XCTFail("Failed to create sounds directory: \(error)")
return
}
let sourceURL = soundsDirectory.appendingPathComponent(
"cmux-custom-notification-sound.metadata-\(UUID().uuidString).wav",
isDirectory: false
)
do {
try Data("test".utf8).write(to: sourceURL, options: .atomic)
} catch {
XCTFail("Failed to write source custom sound file: \(error)")
return
}
defer {
try? fileManager.removeItem(at: sourceURL)
}
defaults.set(NotificationSoundSettings.customFileValue, forKey: NotificationSoundSettings.key)
defaults.set(sourceURL.path, forKey: NotificationSoundSettings.customFilePathKey)
let prepareResult = NotificationSoundSettings.prepareCustomFileForNotifications(path: sourceURL.path)
let stagedName: String
switch prepareResult {
case .success(let name):
stagedName = name
case .failure(let issue):
XCTFail("Expected custom sound preparation success, got \(issue)")
return
}
let stagedURL = soundsDirectory.appendingPathComponent(stagedName, isDirectory: false)
let metadataURL = stagedURL.appendingPathExtension("source-metadata")
defer {
try? fileManager.removeItem(at: stagedURL)
try? fileManager.removeItem(at: metadataURL)
}
XCTAssertTrue(fileManager.fileExists(atPath: stagedURL.path))
XCTAssertTrue(fileManager.fileExists(atPath: metadataURL.path))
}
func testNotificationCustomSoundReturnsNilWhenPreparationFails() {
let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let invalidSourceURL = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-invalid-sound-\(UUID().uuidString).mp3", isDirectory: false)
defer {
try? FileManager.default.removeItem(at: invalidSourceURL)
let stagedURL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Sounds", isDirectory: true)
.appendingPathComponent("cmux-custom-notification-sound.caf", isDirectory: false)
try? FileManager.default.removeItem(at: stagedURL)
}
do {
try Data("not-audio".utf8).write(to: invalidSourceURL, options: .atomic)
} catch {
XCTFail("Failed to write invalid custom sound source: \(error)")
return
}
defaults.set(NotificationSoundSettings.customFileValue, forKey: NotificationSoundSettings.key)
defaults.set(invalidSourceURL.path, forKey: NotificationSoundSettings.customFilePathKey)
XCTAssertNil(NotificationSoundSettings.sound(defaults: defaults))
}
func testNotificationCustomPreparationReportsMissingFile() {
let missingPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-missing-\(UUID().uuidString).wav", isDirectory: false)
.path
let result = NotificationSoundSettings.prepareCustomFileForNotifications(path: missingPath)
switch result {
case .success:
XCTFail("Expected missing file failure")
case .failure(let issue):
guard case .missingFile = issue else {
XCTFail("Expected missingFile issue, got \(issue)")
return
}
}
}
func testNotificationAuthorizationStateMappingCoversKnownUNAuthorizationStatuses() {
XCTAssertEqual(TerminalNotificationStore.authorizationState(from: .notDetermined), .notDetermined)
XCTAssertEqual(TerminalNotificationStore.authorizationState(from: .denied), .denied)
XCTAssertEqual(TerminalNotificationStore.authorizationState(from: .authorized), .authorized)
XCTAssertEqual(TerminalNotificationStore.authorizationState(from: .provisional), .provisional)
}
func testNotificationAuthorizationStateDeliveryCapability() {
XCTAssertFalse(NotificationAuthorizationState.unknown.allowsDelivery)
XCTAssertFalse(NotificationAuthorizationState.notDetermined.allowsDelivery)
XCTAssertFalse(NotificationAuthorizationState.denied.allowsDelivery)
XCTAssertTrue(NotificationAuthorizationState.authorized.allowsDelivery)
XCTAssertTrue(NotificationAuthorizationState.provisional.allowsDelivery)
XCTAssertTrue(NotificationAuthorizationState.ephemeral.allowsDelivery)
}
func testNotificationAuthorizationDefersFirstPromptWhileAppIsInactive() {
XCTAssertTrue(
TerminalNotificationStore.shouldDeferAutomaticAuthorizationRequest(
status: .notDetermined,
isAppActive: false
)
)
XCTAssertFalse(
TerminalNotificationStore.shouldDeferAutomaticAuthorizationRequest(
status: .notDetermined,
isAppActive: true
)
)
XCTAssertFalse(
TerminalNotificationStore.shouldDeferAutomaticAuthorizationRequest(
status: .authorized,
isAppActive: false
)
)
}
func testNotificationAuthorizationRequestGatingAllowsSettingsRetry() {
XCTAssertTrue(
TerminalNotificationStore.shouldRequestAuthorization(
isAutomaticRequest: false,
hasRequestedAutomaticAuthorization: true
)
)
XCTAssertTrue(
TerminalNotificationStore.shouldRequestAuthorization(
isAutomaticRequest: true,
hasRequestedAutomaticAuthorization: false
)
)
XCTAssertFalse(
TerminalNotificationStore.shouldRequestAuthorization(
isAutomaticRequest: true,
hasRequestedAutomaticAuthorization: true
)
)
}
func testNotificationSettingsPromptUsesSheetAndNeverRunsModal() {
let store = TerminalNotificationStore.shared
let alertSpy = NotificationSettingsAlertSpy()
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 320),
styleMask: [.titled],
backing: .buffered,
defer: false
)
var openedURL: URL?
store.configureNotificationSettingsPromptHooksForTesting(
windowProvider: { window },
alertFactory: { alertSpy },
scheduler: { _, block in block() },
urlOpener: { openedURL = $0 }
)
store.promptToEnableNotificationsForTesting()
let drained = expectation(description: "main queue drained")
DispatchQueue.main.async { drained.fulfill() }
wait(for: [drained], timeout: 1.0)
XCTAssertEqual(alertSpy.beginSheetModalCallCount, 1)
XCTAssertEqual(alertSpy.runModalCallCount, 0)
XCTAssertEqual(
openedURL?.absoluteString,
"x-apple.systempreferences:com.apple.preference.notifications"
)
}
func testNotificationSettingsPromptRetriesUntilWindowExists() {
let store = TerminalNotificationStore.shared
let alertSpy = NotificationSettingsAlertSpy()
alertSpy.nextResponse = .alertSecondButtonReturn
var queuedRetryBlocks: [() -> Void] = []
var promptWindow: NSWindow?
store.configureNotificationSettingsPromptHooksForTesting(
windowProvider: { promptWindow },
alertFactory: { alertSpy },
scheduler: { _, block in queuedRetryBlocks.append(block) },
urlOpener: { _ in XCTFail("Should not open settings for Not Now response") }
)
store.promptToEnableNotificationsForTesting()
let drained = expectation(description: "main queue drained")
DispatchQueue.main.async { drained.fulfill() }
wait(for: [drained], timeout: 1.0)
XCTAssertEqual(alertSpy.beginSheetModalCallCount, 0)
XCTAssertEqual(alertSpy.runModalCallCount, 0)
XCTAssertEqual(queuedRetryBlocks.count, 1)
promptWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 320),
styleMask: [.titled],
backing: .buffered,
defer: false
)
queuedRetryBlocks.removeFirst()()
XCTAssertEqual(alertSpy.beginSheetModalCallCount, 1)
XCTAssertEqual(alertSpy.runModalCallCount, 0)
}
func testNotificationIndexesTrackUnreadCountsByTabAndSurface() {
let tabA = UUID()
let tabB = UUID()
let surfaceA = UUID()
let surfaceB = UUID()
let notificationAUnread = TerminalNotification(
id: UUID(),
tabId: tabA,
surfaceId: surfaceA,
title: "A unread",
subtitle: "",
body: "",
createdAt: Date(),
isRead: false
)
let notificationARead = TerminalNotification(
id: UUID(),
tabId: tabA,
surfaceId: surfaceB,
title: "A read",
subtitle: "",
body: "",
createdAt: Date(),
isRead: true
)
let notificationBUnread = TerminalNotification(
id: UUID(),
tabId: tabB,
surfaceId: nil,
title: "B unread",
subtitle: "",
body: "",
createdAt: Date(),
isRead: false
)
let store = TerminalNotificationStore.shared
store.replaceNotificationsForTesting([
notificationAUnread,
notificationARead,
notificationBUnread
])
XCTAssertEqual(store.unreadCount, 2)
XCTAssertEqual(store.unreadCount(forTabId: tabA), 1)
XCTAssertEqual(store.unreadCount(forTabId: tabB), 1)
XCTAssertTrue(store.hasUnreadNotification(forTabId: tabA, surfaceId: surfaceA))
XCTAssertFalse(store.hasUnreadNotification(forTabId: tabA, surfaceId: surfaceB))
XCTAssertTrue(store.hasUnreadNotification(forTabId: tabB, surfaceId: nil))
XCTAssertEqual(store.latestNotification(forTabId: tabA)?.id, notificationAUnread.id)
XCTAssertEqual(store.latestNotification(forTabId: tabB)?.id, notificationBUnread.id)
}
func testNotificationIndexesUpdateAfterReadAndClearMutations() {
let tab = UUID()
let surfaceUnread = UUID()
let surfaceRead = UUID()
let unreadNotification = TerminalNotification(
id: UUID(),
tabId: tab,
surfaceId: surfaceUnread,
title: "Unread",
subtitle: "",
body: "",
createdAt: Date(),
isRead: false
)
let readNotification = TerminalNotification(
id: UUID(),
tabId: tab,
surfaceId: surfaceRead,
title: "Read",
subtitle: "",
body: "",
createdAt: Date(),
isRead: true
)
let store = TerminalNotificationStore.shared
store.replaceNotificationsForTesting([unreadNotification, readNotification])
XCTAssertEqual(store.unreadCount(forTabId: tab), 1)
XCTAssertTrue(store.hasUnreadNotification(forTabId: tab, surfaceId: surfaceUnread))
store.markRead(forTabId: tab, surfaceId: surfaceUnread)
XCTAssertEqual(store.unreadCount(forTabId: tab), 0)
XCTAssertFalse(store.hasUnreadNotification(forTabId: tab, surfaceId: surfaceUnread))
XCTAssertEqual(store.latestNotification(forTabId: tab)?.id, unreadNotification.id)
store.clearNotifications(forTabId: tab)
XCTAssertEqual(store.unreadCount(forTabId: tab), 0)
XCTAssertNil(store.latestNotification(forTabId: tab))
}
}
final class MenuBarBadgeLabelFormatterTests: XCTestCase {
func testBadgeLabelFormatting() {
XCTAssertNil(MenuBarBadgeLabelFormatter.badgeText(for: 0))
XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 1), "1")
XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 9), "9")
XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 10), "9+")
XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 47), "9+")
}
}
final class NotificationMenuSnapshotBuilderTests: XCTestCase {
func testSnapshotCountsUnreadAndLimitsRecentItems() {
let notifications = (0..<8).map { index in
TerminalNotification(
id: UUID(),
tabId: UUID(),
surfaceId: nil,
title: "N\(index)",
subtitle: "",
body: "",
createdAt: Date(timeIntervalSince1970: TimeInterval(index)),
isRead: index.isMultiple(of: 2)
)
}
let snapshot = NotificationMenuSnapshotBuilder.make(
notifications: notifications,
maxInlineNotificationItems: 3
)
XCTAssertEqual(snapshot.unreadCount, 4)
XCTAssertTrue(snapshot.hasNotifications)
XCTAssertTrue(snapshot.hasUnreadNotifications)
XCTAssertEqual(snapshot.recentNotifications.count, 3)
XCTAssertEqual(snapshot.recentNotifications.map(\.id), Array(notifications.prefix(3)).map(\.id))
}
func testStateHintTitleHandlesSingularPluralAndZero() {
XCTAssertEqual(NotificationMenuSnapshotBuilder.stateHintTitle(unreadCount: 0), "No unread notifications")
XCTAssertEqual(NotificationMenuSnapshotBuilder.stateHintTitle(unreadCount: 1), "1 unread notification")
XCTAssertEqual(NotificationMenuSnapshotBuilder.stateHintTitle(unreadCount: 2), "2 unread notifications")
}
}
final class MenuBarBuildHintFormatterTests: XCTestCase {
func testReleaseBuildShowsNoHint() {
XCTAssertNil(MenuBarBuildHintFormatter.menuTitle(appName: "cmux DEV menubar-extra", isDebugBuild: false))
}
func testDebugBuildWithTagShowsTag() {
XCTAssertEqual(
MenuBarBuildHintFormatter.menuTitle(appName: "cmux DEV menubar-extra", isDebugBuild: true),
"Build Tag: menubar-extra"
)
}
func testDebugBuildWithoutTagShowsUntagged() {
XCTAssertEqual(
MenuBarBuildHintFormatter.menuTitle(appName: "cmux DEV", isDebugBuild: true),
"Build: DEV (untagged)"
)
}
}
final class MenuBarNotificationLineFormatterTests: XCTestCase {
func testPlainTitleContainsUnreadDotBodyAndTab() {
let notification = TerminalNotification(
id: UUID(),
tabId: UUID(),
surfaceId: nil,
title: "Build finished",
subtitle: "",
body: "All checks passed",
createdAt: Date(timeIntervalSince1970: 0),
isRead: false
)
let line = MenuBarNotificationLineFormatter.plainTitle(notification: notification, tabTitle: "workspace-1")
XCTAssertTrue(line.hasPrefix("● Build finished"))
XCTAssertTrue(line.contains("All checks passed"))
XCTAssertTrue(line.contains("workspace-1"))
}
func testPlainTitleFallsBackToSubtitleWhenBodyEmpty() {
let notification = TerminalNotification(
id: UUID(),
tabId: UUID(),
surfaceId: nil,
title: "Deploy",
subtitle: "staging",
body: "",
createdAt: Date(timeIntervalSince1970: 0),
isRead: true
)
let line = MenuBarNotificationLineFormatter.plainTitle(notification: notification, tabTitle: nil)
XCTAssertTrue(line.hasPrefix(" Deploy"))
XCTAssertTrue(line.contains("staging"))
}
func testMenuTitleWrapsAndTruncatesToThreeLines() {
let notification = TerminalNotification(
id: UUID(),
tabId: UUID(),
surfaceId: nil,
title: "Extremely long notification title for wrapping behavior validation",
subtitle: "",
body: Array(repeating: "this body should wrap and eventually truncate", count: 8).joined(separator: " "),
createdAt: Date(timeIntervalSince1970: 0),
isRead: false
)
let title = MenuBarNotificationLineFormatter.menuTitle(
notification: notification,
tabTitle: "workspace-with-a-very-long-name",
maxWidth: 120,
maxLines: 3
)
XCTAssertLessThanOrEqual(title.components(separatedBy: "\n").count, 3)
XCTAssertTrue(title.hasSuffix(""))
}
func testMenuTitlePreservesShortTextWithoutEllipsis() {
let notification = TerminalNotification(
id: UUID(),
tabId: UUID(),
surfaceId: nil,
title: "Done",
subtitle: "",
body: "All checks passed",
createdAt: Date(timeIntervalSince1970: 0),
isRead: false
)
let title = MenuBarNotificationLineFormatter.menuTitle(
notification: notification,
tabTitle: "w1",
maxWidth: 320,
maxLines: 3
)
XCTAssertFalse(title.hasSuffix(""))
}
}
final class MenuBarIconDebugSettingsTests: XCTestCase {
func testDisplayedUnreadCountUsesPreviewOverrideWhenEnabled() {
let suiteName = "MenuBarIconDebugSettingsTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(true, forKey: MenuBarIconDebugSettings.previewEnabledKey)
defaults.set(7, forKey: MenuBarIconDebugSettings.previewCountKey)
XCTAssertEqual(MenuBarIconDebugSettings.displayedUnreadCount(actualUnreadCount: 2, defaults: defaults), 7)
}
func testBadgeRenderConfigClampsInvalidValues() {
let suiteName = "MenuBarIconDebugSettingsTests.Clamp.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(-100, forKey: MenuBarIconDebugSettings.badgeRectXKey)
defaults.set(200, forKey: MenuBarIconDebugSettings.badgeRectYKey)
defaults.set(-100, forKey: MenuBarIconDebugSettings.singleDigitFontSizeKey)
defaults.set(100, forKey: MenuBarIconDebugSettings.multiDigitXAdjustKey)
let config = MenuBarIconDebugSettings.badgeRenderConfig(defaults: defaults)
XCTAssertEqual(config.badgeRect.origin.x, 0, accuracy: 0.001)
XCTAssertEqual(config.badgeRect.origin.y, 20, accuracy: 0.001)
XCTAssertEqual(config.singleDigitFontSize, 6, accuracy: 0.001)
XCTAssertEqual(config.multiDigitXAdjust, 4, accuracy: 0.001)
}
func testBadgeRenderConfigUsesLegacySingleDigitXAdjustWhenNewKeyMissing() {
let suiteName = "MenuBarIconDebugSettingsTests.LegacyX.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(2.5, forKey: MenuBarIconDebugSettings.legacySingleDigitXAdjustKey)
let config = MenuBarIconDebugSettings.badgeRenderConfig(defaults: defaults)
XCTAssertEqual(config.singleDigitXAdjust, 2.5, accuracy: 0.001)
}
}
@MainActor
final class MenuBarIconRendererTests: XCTestCase {
func testImageWidthDoesNotShiftWhenBadgeAppears() {
let noBadge = MenuBarIconRenderer.makeImage(unreadCount: 0)
let withBadge = MenuBarIconRenderer.makeImage(unreadCount: 2)
XCTAssertEqual(noBadge.size.width, 18, accuracy: 0.001)
XCTAssertEqual(withBadge.size.width, 18, accuracy: 0.001)
}
}

View file

@ -0,0 +1,857 @@
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 FinderServicePathResolverTests: XCTestCase {
func testOrderedUniqueDirectoriesUsesParentForFilesAndDedupes() {
let input: [URL] = [
URL(fileURLWithPath: "/tmp/cmux-services/project", isDirectory: true),
URL(fileURLWithPath: "/tmp/cmux-services/project/README.md", isDirectory: false),
URL(fileURLWithPath: "/tmp/cmux-services/../cmux-services/project", isDirectory: true),
URL(fileURLWithPath: "/tmp/cmux-services/other", isDirectory: true),
]
let directories = FinderServicePathResolver.orderedUniqueDirectories(from: input)
XCTAssertEqual(
directories,
[
"/tmp/cmux-services/project",
"/tmp/cmux-services/other",
]
)
}
func testOrderedUniqueDirectoriesPreservesFirstSeenOrder() {
let input: [URL] = [
URL(fileURLWithPath: "/tmp/cmux-services/b", isDirectory: true),
URL(fileURLWithPath: "/tmp/cmux-services/a/file.txt", isDirectory: false),
URL(fileURLWithPath: "/tmp/cmux-services/a", isDirectory: true),
URL(fileURLWithPath: "/tmp/cmux-services/b/file.txt", isDirectory: false),
]
let directories = FinderServicePathResolver.orderedUniqueDirectories(from: input)
XCTAssertEqual(
directories,
[
"/tmp/cmux-services/b",
"/tmp/cmux-services/a",
]
)
}
}
final class VSCodeServeWebURLBuilderTests: XCTestCase {
func testExtractWebUIURLParsesServeWebOutput() {
let output = """
*
* Visual Studio Code Server
*
Web UI available at http://127.0.0.1:5555?tkn=test-token
"""
let url = VSCodeServeWebURLBuilder.extractWebUIURL(from: output)
XCTAssertEqual(url?.absoluteString, "http://127.0.0.1:5555?tkn=test-token")
}
func testOpenFolderURLAppendsFolderQueryWhilePreservingToken() {
let baseURL = URL(string: "http://127.0.0.1:5555?tkn=test-token")!
let url = VSCodeServeWebURLBuilder.openFolderURL(
baseWebUIURL: baseURL,
directoryPath: "/Users/tester/Projects/cmux"
)
let components = URLComponents(url: url!, resolvingAgainstBaseURL: false)
XCTAssertEqual(components?.queryItems?.first(where: { $0.name == "tkn" })?.value, "test-token")
XCTAssertEqual(components?.queryItems?.first(where: { $0.name == "folder" })?.value, "/Users/tester/Projects/cmux")
}
func testOpenFolderURLReplacesExistingFolderQuery() {
let baseURL = URL(string: "http://127.0.0.1:5555?tkn=test-token&folder=/tmp/old")!
let url = VSCodeServeWebURLBuilder.openFolderURL(
baseWebUIURL: baseURL,
directoryPath: "/Users/tester/New Folder"
)
let components = URLComponents(url: url!, resolvingAgainstBaseURL: false)
XCTAssertEqual(
components?.queryItems?.filter { $0.name == "folder" }.count,
1
)
XCTAssertEqual(
components?.queryItems?.first(where: { $0.name == "folder" })?.value,
"/Users/tester/New Folder"
)
}
}
final class VSCodeCLILaunchConfigurationBuilderTests: XCTestCase {
func testLaunchConfigurationUsesCodeTunnelBinary() {
let appURL = URL(fileURLWithPath: "/Applications/Visual Studio Code.app", isDirectory: true)
let expectedExecutablePath = "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code-tunnel"
let configuration = VSCodeCLILaunchConfigurationBuilder.launchConfiguration(
vscodeApplicationURL: appURL,
baseEnvironment: [:],
isExecutableAtPath: { $0 == expectedExecutablePath }
)
XCTAssertEqual(configuration?.executableURL.path, expectedExecutablePath)
XCTAssertEqual(configuration?.argumentsPrefix, [])
XCTAssertEqual(configuration?.environment["ELECTRON_RUN_AS_NODE"], "1")
}
func testLaunchConfigurationMapsNodeEnvironmentVariables() {
let configuration = VSCodeCLILaunchConfigurationBuilder.launchConfiguration(
vscodeApplicationURL: URL(fileURLWithPath: "/Applications/Visual Studio Code.app", isDirectory: true),
baseEnvironment: [
"PATH": "/usr/bin:/bin",
"NODE_OPTIONS": "--max-old-space-size=4096",
"NODE_REPL_EXTERNAL_MODULE": "module-name"
],
isExecutableAtPath: { _ in true }
)
XCTAssertEqual(configuration?.environment["PATH"], "/usr/bin:/bin")
XCTAssertEqual(configuration?.environment["VSCODE_NODE_OPTIONS"], "--max-old-space-size=4096")
XCTAssertEqual(configuration?.environment["VSCODE_NODE_REPL_EXTERNAL_MODULE"], "module-name")
XCTAssertNil(configuration?.environment["NODE_OPTIONS"])
XCTAssertNil(configuration?.environment["NODE_REPL_EXTERNAL_MODULE"])
}
func testLaunchConfigurationClearsStaleVSCodeNodeVariablesWhenNodeVariablesAreAbsent() {
let configuration = VSCodeCLILaunchConfigurationBuilder.launchConfiguration(
vscodeApplicationURL: URL(fileURLWithPath: "/Applications/Visual Studio Code.app", isDirectory: true),
baseEnvironment: [
"PATH": "/usr/bin:/bin",
"VSCODE_NODE_OPTIONS": "--stale",
"VSCODE_NODE_REPL_EXTERNAL_MODULE": "stale-module"
],
isExecutableAtPath: { _ in true }
)
XCTAssertEqual(configuration?.environment["PATH"], "/usr/bin:/bin")
XCTAssertNil(configuration?.environment["VSCODE_NODE_OPTIONS"])
XCTAssertNil(configuration?.environment["VSCODE_NODE_REPL_EXTERNAL_MODULE"])
}
}
final class ServeWebOutputCollectorTests: XCTestCase {
func testWaitForURLReturnsFalseAfterProcessExitSignal() {
let collector = ServeWebOutputCollector()
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
collector.markProcessExited()
}
let start = Date()
let resolved = collector.waitForURL(timeoutSeconds: 1)
let elapsed = Date().timeIntervalSince(start)
XCTAssertFalse(resolved)
XCTAssertLessThan(elapsed, 0.5)
}
func testWaitForURLReturnsTrueWhenURLIsCollected() {
let collector = ServeWebOutputCollector()
let urlLine = "Web UI available at http://127.0.0.1:7777?tkn=test-token\n"
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
collector.append(Data(urlLine.utf8))
}
XCTAssertTrue(collector.waitForURL(timeoutSeconds: 1))
XCTAssertEqual(collector.webUIURL?.absoluteString, "http://127.0.0.1:7777?tkn=test-token")
}
func testMarkProcessExitedParsesFinalURLWithoutTrailingNewline() {
let collector = ServeWebOutputCollector()
let finalChunk = "Web UI available at http://127.0.0.1:9001?tkn=final-token"
collector.append(Data(finalChunk.utf8))
collector.markProcessExited()
XCTAssertTrue(collector.waitForURL(timeoutSeconds: 0.1))
XCTAssertEqual(collector.webUIURL?.absoluteString, "http://127.0.0.1:9001?tkn=final-token")
}
}
final class VSCodeServeWebControllerTests: XCTestCase {
func testStopDuringInFlightLaunchDoesNotDropNextGenerationCompletion() {
let firstLaunchStarted = expectation(description: "first launch started")
let firstCompletionCalled = expectation(description: "first generation completion called")
let secondCompletionCalled = expectation(description: "second generation completion called")
let launchGate = DispatchSemaphore(value: 0)
let launchCallLock = NSLock()
var launchCallCount = 0
let controller = VSCodeServeWebController.makeForTesting { _, _ in
launchCallLock.lock()
launchCallCount += 1
let callNumber = launchCallCount
launchCallLock.unlock()
if callNumber == 1 {
firstLaunchStarted.fulfill()
_ = launchGate.wait(timeout: .now() + 1)
}
return nil
}
let callbackLock = NSLock()
var firstGenerationCallbacks: [URL?] = []
var secondGenerationCallbacks: [URL?] = []
let vscodeAppURL = URL(fileURLWithPath: "/Applications/Visual Studio Code.app", isDirectory: true)
controller.ensureServeWebURL(vscodeApplicationURL: vscodeAppURL) { url in
callbackLock.lock()
firstGenerationCallbacks.append(url)
callbackLock.unlock()
firstCompletionCalled.fulfill()
}
wait(for: [firstLaunchStarted], timeout: 1)
controller.stop()
controller.ensureServeWebURL(vscodeApplicationURL: vscodeAppURL) { url in
callbackLock.lock()
secondGenerationCallbacks.append(url)
callbackLock.unlock()
secondCompletionCalled.fulfill()
}
launchGate.signal()
wait(for: [firstCompletionCalled, secondCompletionCalled], timeout: 2)
callbackLock.lock()
let firstSnapshot = firstGenerationCallbacks
let secondSnapshot = secondGenerationCallbacks
callbackLock.unlock()
launchCallLock.lock()
let launchCalls = launchCallCount
launchCallLock.unlock()
XCTAssertEqual(firstSnapshot.count, 1)
if firstSnapshot.count == 1 {
XCTAssertNil(firstSnapshot[0])
}
XCTAssertEqual(secondSnapshot.count, 1)
if secondSnapshot.count == 1 {
XCTAssertNil(secondSnapshot[0])
}
XCTAssertEqual(launchCalls, 2)
}
func testStopRemovesOrphanedConnectionTokenFiles() throws {
let tokenFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: tokenFileURL) }
try Data("token".utf8).write(to: tokenFileURL)
XCTAssertTrue(FileManager.default.fileExists(atPath: tokenFileURL.path))
let controller = VSCodeServeWebController.makeForTesting { _, _ in
XCTFail("Expected no launch")
return nil
}
controller.trackConnectionTokenFileForTesting(tokenFileURL)
controller.stop()
XCTAssertFalse(FileManager.default.fileExists(atPath: tokenFileURL.path))
}
}
final class OmnibarStateMachineTests: XCTestCase {
func testEscapeRevertsWhenEditingThenBlursOnSecondEscape() throws {
var state = OmnibarState()
var effects = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/"))
XCTAssertTrue(state.isFocused)
XCTAssertEqual(state.buffer, "https://example.com/")
XCTAssertFalse(state.isUserEditing)
XCTAssertTrue(effects.shouldSelectAll)
effects = omnibarReduce(state: &state, event: .bufferChanged("exam"))
XCTAssertTrue(state.isUserEditing)
XCTAssertEqual(state.buffer, "exam")
XCTAssertTrue(effects.shouldRefreshSuggestions)
// Simulate an open popup.
effects = omnibarReduce(
state: &state,
event: .suggestionsUpdated([.search(engineName: "Google", query: "exam")])
)
XCTAssertEqual(state.suggestions.count, 1)
XCTAssertFalse(effects.shouldSelectAll)
// First escape: revert + close popup + select-all.
effects = omnibarReduce(state: &state, event: .escape)
XCTAssertEqual(state.buffer, "https://example.com/")
XCTAssertFalse(state.isUserEditing)
XCTAssertTrue(state.suggestions.isEmpty)
XCTAssertTrue(effects.shouldSelectAll)
XCTAssertFalse(effects.shouldBlurToWebView)
// Second escape: blur (since we're not editing and popup is closed).
effects = omnibarReduce(state: &state, event: .escape)
XCTAssertTrue(effects.shouldBlurToWebView)
}
func testPanelURLChangeDoesNotClobberUserBufferWhileEditing() throws {
var state = OmnibarState()
_ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://a.test/"))
_ = omnibarReduce(state: &state, event: .bufferChanged("hello"))
XCTAssertTrue(state.isUserEditing)
_ = omnibarReduce(state: &state, event: .panelURLChanged(currentURLString: "https://b.test/"))
XCTAssertEqual(state.currentURLString, "https://b.test/")
XCTAssertEqual(state.buffer, "hello")
XCTAssertTrue(state.isUserEditing)
let effects = omnibarReduce(state: &state, event: .escape)
XCTAssertEqual(state.buffer, "https://b.test/")
XCTAssertTrue(effects.shouldSelectAll)
}
func testFocusLostRevertsUnlessSuppressed() throws {
var state = OmnibarState()
_ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/"))
_ = omnibarReduce(state: &state, event: .bufferChanged("typed"))
XCTAssertEqual(state.buffer, "typed")
_ = omnibarReduce(state: &state, event: .focusLostPreserveBuffer(currentURLString: "https://example.com/"))
XCTAssertEqual(state.buffer, "typed")
_ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/"))
_ = omnibarReduce(state: &state, event: .bufferChanged("typed2"))
_ = omnibarReduce(state: &state, event: .focusLostRevertBuffer(currentURLString: "https://example.com/"))
XCTAssertEqual(state.buffer, "https://example.com/")
}
func testSuggestionsUpdateKeepsSelectionAcrossNonEmptyListRefresh() throws {
var state = OmnibarState()
_ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/"))
_ = omnibarReduce(state: &state, event: .bufferChanged("go"))
let base: [OmnibarSuggestion] = [
.search(engineName: "Google", query: "go"),
.remoteSearchSuggestion("go tutorial"),
.remoteSearchSuggestion("go json"),
]
_ = omnibarReduce(state: &state, event: .suggestionsUpdated(base))
XCTAssertEqual(state.selectedSuggestionIndex, 0)
_ = omnibarReduce(state: &state, event: .moveSelection(delta: 2))
XCTAssertEqual(state.selectedSuggestionIndex, 2)
// Simulate remote merge update for the same query while popup remains open.
let merged: [OmnibarSuggestion] = [
.search(engineName: "Google", query: "go"),
.remoteSearchSuggestion("go tutorial"),
.remoteSearchSuggestion("go json"),
.remoteSearchSuggestion("go fmt"),
]
_ = omnibarReduce(state: &state, event: .suggestionsUpdated(merged))
XCTAssertEqual(state.selectedSuggestionIndex, 2, "Expected selection to remain stable while list stays open")
}
func testSuggestionsReopenResetsSelectionToFirstRow() throws {
var state = OmnibarState()
_ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/"))
_ = omnibarReduce(state: &state, event: .bufferChanged("go"))
let rows: [OmnibarSuggestion] = [
.search(engineName: "Google", query: "go"),
.remoteSearchSuggestion("go tutorial"),
]
_ = omnibarReduce(state: &state, event: .suggestionsUpdated(rows))
_ = omnibarReduce(state: &state, event: .moveSelection(delta: 1))
XCTAssertEqual(state.selectedSuggestionIndex, 1)
_ = omnibarReduce(state: &state, event: .suggestionsUpdated([]))
XCTAssertEqual(state.selectedSuggestionIndex, 0)
_ = omnibarReduce(state: &state, event: .suggestionsUpdated(rows))
XCTAssertEqual(state.selectedSuggestionIndex, 0, "Expected reopened popup to focus first row")
}
func testSuggestionsUpdatePrefersAutocompleteMatchWhenSelectionNotTracked() throws {
var state = OmnibarState()
_ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/"))
_ = omnibarReduce(state: &state, event: .bufferChanged("gm"))
let rows: [OmnibarSuggestion] = [
.search(engineName: "Google", query: "gm"),
.history(url: "https://google.com/", title: "Google"),
.history(url: "https://gmail.com/", title: "Gmail"),
]
_ = omnibarReduce(state: &state, event: .suggestionsUpdated(rows))
XCTAssertEqual(state.selectedSuggestionIndex, 2, "Expected autocomplete candidate to become selected without explicit index state.")
XCTAssertEqual(state.selectedSuggestionID, rows[2].id)
XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: state.suggestions[state.selectedSuggestionIndex]))
XCTAssertEqual(state.suggestions[state.selectedSuggestionIndex].completion, "https://gmail.com/")
}
}
final class OmnibarRemoteSuggestionMergeTests: XCTestCase {
func testMergeRemoteSuggestionsInsertsBelowSearchAndDedupes() {
let now = Date()
let entries: [BrowserHistoryStore.Entry] = [
BrowserHistoryStore.Entry(
id: UUID(),
url: "https://go.dev/",
title: "The Go Programming Language",
lastVisited: now,
visitCount: 10
),
]
let merged = buildOmnibarSuggestions(
query: "go",
engineName: "Google",
historyEntries: entries,
openTabMatches: [],
remoteQueries: ["go tutorial", "go.dev", "go json"],
resolvedURL: nil,
limit: 8
)
let completions = merged.compactMap { $0.completion }
XCTAssertGreaterThanOrEqual(completions.count, 5)
XCTAssertEqual(completions[0], "https://go.dev/")
XCTAssertEqual(completions[1], "go")
let remoteCompletions = Array(completions.dropFirst(2))
XCTAssertEqual(Set(remoteCompletions), Set(["go tutorial", "go.dev", "go json"]))
XCTAssertEqual(remoteCompletions.count, 3)
}
func testStaleRemoteSuggestionsKeptForNearbyEdits() {
let stale = staleOmnibarRemoteSuggestionsForDisplay(
query: "go t",
previousRemoteQuery: "go",
previousRemoteSuggestions: ["go tutorial", "go json", "golang tips"],
limit: 8
)
XCTAssertEqual(stale, ["go tutorial", "go json", "golang tips"])
}
func testStaleRemoteSuggestionsTrimAndRespectLimit() {
let stale = staleOmnibarRemoteSuggestionsForDisplay(
query: "gooo",
previousRemoteQuery: "goo",
previousRemoteSuggestions: [" go tutorial ", "", "go json", " ", "go fmt"],
limit: 2
)
XCTAssertEqual(stale, ["go tutorial", "go json"])
}
func testStaleRemoteSuggestionsDroppedForUnrelatedQuery() {
let stale = staleOmnibarRemoteSuggestionsForDisplay(
query: "python",
previousRemoteQuery: "go",
previousRemoteSuggestions: ["go tutorial", "go json"],
limit: 8
)
XCTAssertTrue(stale.isEmpty)
}
}
final class OmnibarSuggestionRankingTests: XCTestCase {
private var fixedNow: Date {
Date(timeIntervalSinceReferenceDate: 10_000_000)
}
func testSingleCharacterQueryPromotesAutocompletionMatchToFirstRow() {
let entries: [BrowserHistoryStore.Entry] = [
.init(
id: UUID(),
url: "https://news.ycombinator.com/",
title: "News.YC",
lastVisited: fixedNow,
visitCount: 12,
typedCount: 1,
lastTypedAt: fixedNow
),
.init(
id: UUID(),
url: "https://www.google.com/",
title: "Google",
lastVisited: fixedNow - 200,
visitCount: 8,
typedCount: 2,
lastTypedAt: fixedNow - 200
),
]
let results = buildOmnibarSuggestions(
query: "n",
engineName: "Google",
historyEntries: entries,
openTabMatches: [],
remoteQueries: ["search google for n", "news"],
resolvedURL: nil,
limit: 8,
now: fixedNow
)
XCTAssertEqual(results.first?.completion, "https://news.ycombinator.com/")
XCTAssertNotEqual(results.map(\.completion).first, "n")
XCTAssertTrue(results.first.map { omnibarSuggestionSupportsAutocompletion(query: "n", suggestion: $0) } ?? false)
}
func testGmAutocompleteCandidateIsFirstOnExactQueryMatch() {
let entries: [BrowserHistoryStore.Entry] = [
.init(
id: UUID(),
url: "https://google.com/",
title: "Google",
lastVisited: fixedNow,
visitCount: 4,
typedCount: 1,
lastTypedAt: fixedNow
),
.init(
id: UUID(),
url: "https://gmail.com/",
title: "Gmail",
lastVisited: fixedNow,
visitCount: 10,
typedCount: 2,
lastTypedAt: fixedNow
),
]
let results = buildOmnibarSuggestions(
query: "gm",
engineName: "Google",
historyEntries: entries,
openTabMatches: [],
remoteQueries: ["gmail", "gmail.com", "google mail"],
resolvedURL: nil,
limit: 8,
now: fixedNow
)
XCTAssertEqual(results.first?.completion, "https://gmail.com/")
XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: results[0]))
let inlineCompletion = omnibarInlineCompletionForDisplay(
typedText: "gm",
suggestions: results,
isFocused: true,
selectionRange: NSRange(location: 2, length: 0),
hasMarkedText: false
)
XCTAssertNotNil(inlineCompletion)
}
func testAutocompletionCandidateWinsOverRemoteAndSearchRowsForTwoLetterQuery() {
let entries: [BrowserHistoryStore.Entry] = [
.init(
id: UUID(),
url: "https://google.com/",
title: "Google",
lastVisited: fixedNow,
visitCount: 4,
typedCount: 1,
lastTypedAt: fixedNow
),
.init(
id: UUID(),
url: "https://gmail.com/",
title: "Gmail",
lastVisited: fixedNow,
visitCount: 10,
typedCount: 2,
lastTypedAt: fixedNow
),
]
let results = buildOmnibarSuggestions(
query: "gm",
engineName: "Google",
historyEntries: entries,
openTabMatches: [
.init(
tabId: UUID(),
panelId: UUID(),
url: "https://gmail.com/",
title: "Gmail",
isKnownOpenTab: true
),
],
remoteQueries: ["Search google for gm", "gmail", "gmail.com", "Google mail"],
resolvedURL: nil,
limit: 8,
now: fixedNow
)
XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: results[0]))
XCTAssertEqual(results.first?.completion, "https://gmail.com/")
}
func testSuggestionSelectionPrefersAutocompletionCandidateAfterSuggestionsUpdate() {
let entries: [BrowserHistoryStore.Entry] = [
.init(
id: UUID(),
url: "https://google.com/",
title: "Google",
lastVisited: fixedNow,
visitCount: 4,
typedCount: 1,
lastTypedAt: fixedNow
),
.init(
id: UUID(),
url: "https://gmail.com/",
title: "Gmail",
lastVisited: fixedNow,
visitCount: 10,
typedCount: 2,
lastTypedAt: fixedNow
),
]
let results = buildOmnibarSuggestions(
query: "gm",
engineName: "Google",
historyEntries: entries,
openTabMatches: [],
remoteQueries: ["Search google for gm", "gmail", "gmail.com"],
resolvedURL: nil,
limit: 8,
now: fixedNow
)
var state = OmnibarState()
let _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: ""))
let _ = omnibarReduce(state: &state, event: .bufferChanged("gm"))
let _ = omnibarReduce(state: &state, event: .suggestionsUpdated(results))
XCTAssertEqual(state.selectedSuggestionIndex, 0)
XCTAssertEqual(state.selectedSuggestionID, results[0].id)
XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: state.suggestions[0]))
}
func testTwoCharQueryWithRemoteSuggestionsStillPromotesAutocompletionMatch() {
let entries: [BrowserHistoryStore.Entry] = [
.init(
id: UUID(),
url: "https://news.ycombinator.com/",
title: "News.YC",
lastVisited: fixedNow,
visitCount: 12,
typedCount: 1,
lastTypedAt: fixedNow
),
.init(
id: UUID(),
url: "https://www.google.com/",
title: "Google",
lastVisited: fixedNow - 200,
visitCount: 8,
typedCount: 2,
lastTypedAt: fixedNow - 200
),
]
let results = buildOmnibarSuggestions(
query: "ne",
engineName: "Google",
historyEntries: entries,
openTabMatches: [],
remoteQueries: ["netflix", "new york times", "newegg"],
resolvedURL: nil,
limit: 8,
now: fixedNow
)
// The autocompletable history entry (news.ycombinator.com) should be first despite remote results.
XCTAssertEqual(results.first?.completion, "https://news.ycombinator.com/")
XCTAssertTrue(results.first.map { omnibarSuggestionSupportsAutocompletion(query: "ne", suggestion: $0) } ?? false)
// Remote suggestions should still appear in the results (two-char queries include them).
let remoteCompletions = results.filter {
if case .remote = $0.kind { return true }
return false
}.map(\.completion)
XCTAssertFalse(remoteCompletions.isEmpty, "Expected remote suggestions to be present for two-char query")
}
func testGmQueryWithRemoteSuggestionsAndOpenTabPromotesAutocompletionMatch() {
let entries: [BrowserHistoryStore.Entry] = [
.init(
id: UUID(),
url: "https://google.com/",
title: "Google",
lastVisited: fixedNow,
visitCount: 4,
typedCount: 1,
lastTypedAt: fixedNow
),
.init(
id: UUID(),
url: "https://gmail.com/",
title: "Gmail",
lastVisited: fixedNow,
visitCount: 10,
typedCount: 2,
lastTypedAt: fixedNow
),
]
let results = buildOmnibarSuggestions(
query: "gm",
engineName: "Google",
historyEntries: entries,
openTabMatches: [
.init(
tabId: UUID(),
panelId: UUID(),
url: "https://google.com/maps",
title: "Google Maps",
isKnownOpenTab: true
),
],
remoteQueries: ["gmail login", "gm stock price", "gmail.com"],
resolvedURL: nil,
limit: 8,
now: fixedNow
)
// Gmail should be first (autocompletable + typed history).
XCTAssertEqual(results.first?.completion, "https://gmail.com/")
XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: results[0]))
// Verify remote suggestions are present alongside history/tab matches.
let remoteCompletions = results.filter {
if case .remote = $0.kind { return true }
return false
}.map(\.completion)
XCTAssertFalse(remoteCompletions.isEmpty, "Expected remote suggestions in results")
let hasSearch = results.contains {
if case .search = $0.kind { return true }
return false
}
XCTAssertTrue(hasSearch, "Expected search row in results")
}
func testHistorySuggestionDisplaysTitleAndUrlOnSingleLine() {
let row = OmnibarSuggestion.history(
url: "https://www.example.com/path?q=1",
title: "Example Domain"
)
XCTAssertEqual(row.listText, "Example Domain — example.com/path?q=1")
XCTAssertFalse(row.listText.contains("\n"))
}
func testPublishedBufferTextUsesTypedPrefixWhenInlineSuffixIsSelected() {
let inline = OmnibarInlineCompletion(
typedText: "l",
displayText: "localhost:3000",
acceptedText: "https://localhost:3000/"
)
let published = omnibarPublishedBufferTextForFieldChange(
fieldValue: inline.displayText,
inlineCompletion: inline,
selectionRange: inline.suffixRange,
hasMarkedText: false
)
XCTAssertEqual(published, "l")
}
func testPublishedBufferTextKeepsUserTypedValueWhenDisplayDiffersFromInlineText() {
let inline = OmnibarInlineCompletion(
typedText: "l",
displayText: "localhost:3000",
acceptedText: "https://localhost:3000/"
)
let published = omnibarPublishedBufferTextForFieldChange(
fieldValue: "la",
inlineCompletion: inline,
selectionRange: NSRange(location: 2, length: 0),
hasMarkedText: false
)
XCTAssertEqual(published, "la")
}
func testInlineCompletionRenderIgnoresStaleTypedPrefixMismatch() {
let staleInline = OmnibarInlineCompletion(
typedText: "g",
displayText: "github.com",
acceptedText: "https://github.com/"
)
let active = omnibarInlineCompletionIfBufferMatchesTypedPrefix(
bufferText: "l",
inlineCompletion: staleInline
)
XCTAssertNil(active)
}
func testInlineCompletionRenderKeepsMatchingTypedPrefix() {
let inline = OmnibarInlineCompletion(
typedText: "l",
displayText: "localhost:3000",
acceptedText: "https://localhost:3000/"
)
let active = omnibarInlineCompletionIfBufferMatchesTypedPrefix(
bufferText: "l",
inlineCompletion: inline
)
XCTAssertEqual(active, inline)
}
func testInlineCompletionSkipsTitleMatchWhoseURLDoesNotStartWithTypedText() {
// History entry: visited google.com/search?q=localhost:3000 with title
// "localhost:3000 - Google Search". Typing "l" should NOT inline-complete
// to "google.com/..." because that replaces the typed "l" with "g".
let suggestions: [OmnibarSuggestion] = [
.history(
url: "https://www.google.com/search?q=localhost:3000",
title: "localhost:3000 - Google Search"
),
]
let result = omnibarInlineCompletionForDisplay(
typedText: "l",
suggestions: suggestions,
isFocused: true,
selectionRange: NSRange(location: 1, length: 0),
hasMarkedText: false
)
XCTAssertNil(result, "Should not inline-complete when display text does not start with typed prefix")
}
}

View file

@ -0,0 +1,965 @@
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 SplitShortcutTransientFocusGuardTests: XCTestCase {
func testSuppressesWhenFirstResponderFallsBackAndHostedViewIsTiny() {
XCTAssertTrue(
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
firstResponderIsWindow: true,
hostedSize: CGSize(width: 79, height: 0),
hostedHiddenInHierarchy: false,
hostedAttachedToWindow: true
)
)
}
func testSuppressesWhenFirstResponderFallsBackAndHostedViewIsDetached() {
XCTAssertTrue(
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
firstResponderIsWindow: true,
hostedSize: CGSize(width: 1051.5, height: 1207),
hostedHiddenInHierarchy: false,
hostedAttachedToWindow: false
)
)
}
func testAllowsWhenFirstResponderFallsBackButGeometryIsHealthy() {
XCTAssertFalse(
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
firstResponderIsWindow: true,
hostedSize: CGSize(width: 1051.5, height: 1207),
hostedHiddenInHierarchy: false,
hostedAttachedToWindow: true
)
)
}
func testAllowsWhenFirstResponderIsTerminalEvenIfViewIsTiny() {
XCTAssertFalse(
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
firstResponderIsWindow: false,
hostedSize: CGSize(width: 79, height: 0),
hostedHiddenInHierarchy: false,
hostedAttachedToWindow: true
)
)
}
}
final class FullScreenShortcutTests: XCTestCase {
func testMatchesCommandControlF() {
XCTAssertTrue(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control],
chars: "f",
keyCode: 3
)
)
}
func testMatchesCommandControlFFromKeyCodeWhenCharsAreUnavailable() {
XCTAssertTrue(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control],
chars: "",
keyCode: 3,
layoutCharacterProvider: { _, _ in nil }
)
)
}
func testDoesNotFallbackToANSIWhenLayoutTranslationReturnsNonFCharacter() {
XCTAssertFalse(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control],
chars: "",
keyCode: 3,
layoutCharacterProvider: { _, _ in "u" }
)
)
}
func testMatchesCommandControlFWhenCommandAwareLayoutTranslationProvidesF() {
XCTAssertTrue(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control],
chars: "",
keyCode: 3,
layoutCharacterProvider: { _, modifierFlags in
modifierFlags.contains(.command) ? "f" : "u"
}
)
)
}
func testMatchesCommandControlFWhenCharsAreControlSequence() {
XCTAssertTrue(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control],
chars: "\u{06}",
keyCode: 3,
layoutCharacterProvider: { _, _ in nil }
)
)
}
func testRejectsPhysicalFWhenCharacterRepresentsDifferentLayoutKey() {
XCTAssertFalse(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control],
chars: "u",
keyCode: 3
)
)
}
func testIgnoresCapsLockForCommandControlF() {
XCTAssertTrue(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control, .capsLock],
chars: "f",
keyCode: 3
)
)
}
func testRejectsWhenControlIsMissing() {
XCTAssertFalse(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command],
chars: "f",
keyCode: 3
)
)
}
func testRejectsAdditionalModifiers() {
XCTAssertFalse(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control, .shift],
chars: "f",
keyCode: 3
)
)
XCTAssertFalse(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control, .option],
chars: "f",
keyCode: 3
)
)
}
func testRejectsWhenCommandIsMissing() {
XCTAssertFalse(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.control],
chars: "f",
keyCode: 3
)
)
}
func testRejectsNonFKey() {
XCTAssertFalse(
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
flags: [.command, .control],
chars: "r",
keyCode: 15
)
)
}
}
final class CommandPaletteKeyboardNavigationTests: XCTestCase {
func testArrowKeysMoveSelectionWithoutModifiers() {
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [],
chars: "",
keyCode: 125
),
1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [],
chars: "",
keyCode: 126
),
-1
)
XCTAssertNil(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.shift],
chars: "",
keyCode: 125
)
)
}
func testControlLetterNavigationSupportsPrintableAndControlChars() {
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "n",
keyCode: 45
),
1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "\u{0e}",
keyCode: 45
),
1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "p",
keyCode: 35
),
-1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "\u{10}",
keyCode: 35
),
-1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "j",
keyCode: 38
),
1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "\u{0a}",
keyCode: 38
),
1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "k",
keyCode: 40
),
-1
)
XCTAssertEqual(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "\u{0b}",
keyCode: 40
),
-1
)
}
func testIgnoresUnsupportedModifiersAndKeys() {
XCTAssertNil(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.command],
chars: "n",
keyCode: 45
)
)
XCTAssertNil(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control, .shift],
chars: "n",
keyCode: 45
)
)
XCTAssertNil(
commandPaletteSelectionDeltaForKeyboardNavigation(
flags: [.control],
chars: "x",
keyCode: 7
)
)
}
}
final class CommandPaletteOpenShortcutConsumptionTests: XCTestCase {
func testDoesNotConsumeWhenPaletteIsNotVisible() {
XCTAssertFalse(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: false,
normalizedFlags: [.command],
chars: "n",
keyCode: 45
)
)
}
func testConsumesAppCommandShortcutsWhenPaletteIsVisible() {
XCTAssertTrue(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command],
chars: "n",
keyCode: 45
)
)
XCTAssertTrue(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command],
chars: "t",
keyCode: 17
)
)
XCTAssertTrue(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command, .shift],
chars: ",",
keyCode: 43
)
)
}
func testAllowsClipboardAndUndoShortcutsForPaletteTextEditing() {
XCTAssertFalse(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command],
chars: "v",
keyCode: 9
)
)
XCTAssertFalse(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command],
chars: "z",
keyCode: 6
)
)
XCTAssertFalse(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command, .shift],
chars: "z",
keyCode: 6
)
)
}
func testAllowsArrowAndDeleteEditingCommandsForPaletteTextEditing() {
XCTAssertFalse(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command],
chars: "",
keyCode: 123
)
)
XCTAssertFalse(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [.command],
chars: "",
keyCode: 51
)
)
}
func testConsumesEscapeWhenPaletteIsVisible() {
XCTAssertTrue(
shouldConsumeShortcutWhileCommandPaletteVisible(
isCommandPaletteVisible: true,
normalizedFlags: [],
chars: "",
keyCode: 53
)
)
}
}
final class CommandPaletteRestoreFocusStateMachineTests: XCTestCase {
func testRestoresBrowserAddressBarWhenPaletteOpenedFromFocusedAddressBar() {
let panelId = UUID()
XCTAssertTrue(
ContentView.shouldRestoreBrowserAddressBarAfterCommandPaletteDismiss(
focusedPanelIsBrowser: true,
focusedBrowserAddressBarPanelId: panelId,
focusedPanelId: panelId
)
)
}
func testDoesNotRestoreBrowserAddressBarWhenFocusedPanelIsNotBrowser() {
let panelId = UUID()
XCTAssertFalse(
ContentView.shouldRestoreBrowserAddressBarAfterCommandPaletteDismiss(
focusedPanelIsBrowser: false,
focusedBrowserAddressBarPanelId: panelId,
focusedPanelId: panelId
)
)
}
func testDoesNotRestoreBrowserAddressBarWhenAnotherPanelHadAddressBarFocus() {
XCTAssertFalse(
ContentView.shouldRestoreBrowserAddressBarAfterCommandPaletteDismiss(
focusedPanelIsBrowser: true,
focusedBrowserAddressBarPanelId: UUID(),
focusedPanelId: UUID()
)
)
}
}
final class CommandPaletteRenameSelectionSettingsTests: XCTestCase {
private let suiteName = "cmux.tests.commandPaletteRenameSelection.\(UUID().uuidString)"
private func makeDefaults() -> UserDefaults {
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return defaults
}
func testDefaultsToSelectAllWhenUnset() {
let defaults = makeDefaults()
XCTAssertTrue(CommandPaletteRenameSelectionSettings.selectAllOnFocusEnabled(defaults: defaults))
}
func testReturnsFalseWhenStoredFalse() {
let defaults = makeDefaults()
defaults.set(false, forKey: CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
XCTAssertFalse(CommandPaletteRenameSelectionSettings.selectAllOnFocusEnabled(defaults: defaults))
}
func testReturnsTrueWhenStoredTrue() {
let defaults = makeDefaults()
defaults.set(true, forKey: CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
XCTAssertTrue(CommandPaletteRenameSelectionSettings.selectAllOnFocusEnabled(defaults: defaults))
}
}
final class CommandPaletteSelectionScrollBehaviorTests: XCTestCase {
func testFirstEntryPinsToTopAnchor() {
let anchor = ContentView.commandPaletteScrollPositionAnchor(
selectedIndex: 0,
resultCount: 20
)
XCTAssertEqual(anchor, UnitPoint.top)
}
func testLastEntryPinsToBottomAnchor() {
let anchor = ContentView.commandPaletteScrollPositionAnchor(
selectedIndex: 19,
resultCount: 20
)
XCTAssertEqual(anchor, UnitPoint.bottom)
}
func testMiddleEntryUsesNilAnchorForMinimalScroll() {
let anchor = ContentView.commandPaletteScrollPositionAnchor(
selectedIndex: 6,
resultCount: 20
)
XCTAssertNil(anchor)
}
func testEmptyResultsProduceNoAnchor() {
let anchor = ContentView.commandPaletteScrollPositionAnchor(
selectedIndex: 0,
resultCount: 0
)
XCTAssertNil(anchor)
}
}
final class ShortcutHintModifierPolicyTests: XCTestCase {
func testShortcutHintRequiresEnabledCommandOnlyModifier() {
withDefaultsSuite { defaults in
defaults.set(true, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
XCTAssertTrue(ShortcutHintModifierPolicy.shouldShowHints(for: [.command], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command, .shift], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control, .shift], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command, .option], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control, .option], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command, .control], defaults: defaults))
}
}
func testCommandHintCanBeDisabledInSettings() {
withDefaultsSuite { defaults in
defaults.set(false, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control], defaults: defaults))
}
}
func testCommandHintDefaultsToEnabledWhenSettingMissing() {
withDefaultsSuite { defaults in
defaults.removeObject(forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
XCTAssertTrue(ShortcutHintModifierPolicy.shouldShowHints(for: [.command], defaults: defaults))
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control], defaults: defaults))
}
}
func testShortcutHintUsesIntentionalHoldDelay() {
XCTAssertEqual(ShortcutHintModifierPolicy.intentionalHoldDelay, 0.30, accuracy: 0.001)
}
func testCurrentWindowRequiresHostWindowToBeKeyAndMatchEventWindow() {
XCTAssertTrue(
ShortcutHintModifierPolicy.isCurrentWindow(
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: 42,
keyWindowNumber: 42
)
)
XCTAssertFalse(
ShortcutHintModifierPolicy.isCurrentWindow(
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: 7,
keyWindowNumber: 42
)
)
XCTAssertFalse(
ShortcutHintModifierPolicy.isCurrentWindow(
hostWindowNumber: 42,
hostWindowIsKey: false,
eventWindowNumber: 42,
keyWindowNumber: 42
)
)
}
func testWindowScopedShortcutHintsUseKeyWindowWhenNoEventWindowIsAvailable() {
withDefaultsSuite { defaults in
defaults.set(true, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
XCTAssertTrue(
ShortcutHintModifierPolicy.shouldShowHints(
for: [.command],
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: nil,
keyWindowNumber: 42,
defaults: defaults
)
)
XCTAssertFalse(
ShortcutHintModifierPolicy.shouldShowHints(
for: [.command],
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: nil,
keyWindowNumber: 7,
defaults: defaults
)
)
XCTAssertTrue(
ShortcutHintModifierPolicy.shouldShowHints(
for: [.command],
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: nil,
keyWindowNumber: 42,
defaults: defaults
)
)
XCTAssertFalse(
ShortcutHintModifierPolicy.shouldShowHints(
for: [.control],
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: nil,
keyWindowNumber: 42,
defaults: defaults
)
)
}
}
private func withDefaultsSuite(_ body: (UserDefaults) -> Void) {
let suiteName = "ShortcutHintModifierPolicyTests-\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create defaults suite")
return
}
defaults.removePersistentDomain(forName: suiteName)
body(defaults)
defaults.removePersistentDomain(forName: suiteName)
}
}
final class ShortcutHintDebugSettingsTests: XCTestCase {
func testClampKeepsValuesWithinSupportedRange() {
XCTAssertEqual(ShortcutHintDebugSettings.clamped(0.0), 0.0)
XCTAssertEqual(ShortcutHintDebugSettings.clamped(4.0), 4.0)
XCTAssertEqual(ShortcutHintDebugSettings.clamped(-100.0), ShortcutHintDebugSettings.offsetRange.lowerBound)
XCTAssertEqual(ShortcutHintDebugSettings.clamped(100.0), ShortcutHintDebugSettings.offsetRange.upperBound)
}
func testDefaultOffsetsMatchCurrentBadgePlacements() {
XCTAssertEqual(ShortcutHintDebugSettings.defaultSidebarHintX, 0.0)
XCTAssertEqual(ShortcutHintDebugSettings.defaultSidebarHintY, 0.0)
XCTAssertEqual(ShortcutHintDebugSettings.defaultTitlebarHintX, 4.0)
XCTAssertEqual(ShortcutHintDebugSettings.defaultTitlebarHintY, 0.0)
XCTAssertEqual(ShortcutHintDebugSettings.defaultPaneHintX, 0.0)
XCTAssertEqual(ShortcutHintDebugSettings.defaultPaneHintY, 0.0)
XCTAssertFalse(ShortcutHintDebugSettings.defaultAlwaysShowHints)
XCTAssertTrue(ShortcutHintDebugSettings.defaultShowHintsOnCommandHold)
}
func testShowHintsOnCommandHoldSettingRespectsStoredValue() {
let suiteName = "ShortcutHintDebugSettingsTests-\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create defaults suite")
return
}
defaults.removePersistentDomain(forName: suiteName)
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.removeObject(forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
XCTAssertTrue(ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults))
defaults.set(false, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
XCTAssertFalse(ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults))
defaults.set(true, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
XCTAssertTrue(ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults))
}
func testResetVisibilityDefaultsRestoresAlwaysShowAndCommandHoldFlags() {
let suiteName = "ShortcutHintDebugSettingsTests-\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create defaults suite")
return
}
defaults.removePersistentDomain(forName: suiteName)
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(true, forKey: ShortcutHintDebugSettings.alwaysShowHintsKey)
defaults.set(false, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
ShortcutHintDebugSettings.resetVisibilityDefaults(defaults: defaults)
XCTAssertEqual(
defaults.object(forKey: ShortcutHintDebugSettings.alwaysShowHintsKey) as? Bool,
ShortcutHintDebugSettings.defaultAlwaysShowHints
)
XCTAssertEqual(
defaults.object(forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey) as? Bool,
ShortcutHintDebugSettings.defaultShowHintsOnCommandHold
)
}
}
final class DevBuildBannerDebugSettingsTests: XCTestCase {
func testShowSidebarBannerDefaultsToVisible() {
let suiteName = "DevBuildBannerDebugSettingsTests.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: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
XCTAssertTrue(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
}
func testShowSidebarBannerRespectsStoredValue() {
let suiteName = "DevBuildBannerDebugSettingsTests.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: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
XCTAssertFalse(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
defaults.set(true, forKey: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
XCTAssertTrue(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
}
}
final class ShortcutHintLanePlannerTests: XCTestCase {
func testAssignLanesKeepsSeparatedIntervalsOnSingleLane() {
let intervals: [ClosedRange<CGFloat>] = [0...20, 28...40, 48...64]
XCTAssertEqual(ShortcutHintLanePlanner.assignLanes(for: intervals, minSpacing: 4), [0, 0, 0])
}
func testAssignLanesStacksOverlappingIntervalsIntoAdditionalLanes() {
let intervals: [ClosedRange<CGFloat>] = [0...20, 18...34, 22...38, 40...56]
XCTAssertEqual(ShortcutHintLanePlanner.assignLanes(for: intervals, minSpacing: 4), [0, 1, 2, 0])
}
}
final class ShortcutHintHorizontalPlannerTests: XCTestCase {
func testAssignRightEdgesResolvesOverlapWithMinimumSpacing() {
let intervals: [ClosedRange<CGFloat>] = [0...20, 18...34, 30...46]
let rightEdges = ShortcutHintHorizontalPlanner.assignRightEdges(for: intervals, minSpacing: 6)
XCTAssertEqual(rightEdges.count, intervals.count)
let adjustedIntervals = zip(intervals, rightEdges).map { interval, rightEdge in
let width = interval.upperBound - interval.lowerBound
return (rightEdge - width)...rightEdge
}
XCTAssertGreaterThanOrEqual(adjustedIntervals[1].lowerBound - adjustedIntervals[0].upperBound, 6)
XCTAssertGreaterThanOrEqual(adjustedIntervals[2].lowerBound - adjustedIntervals[1].upperBound, 6)
}
func testAssignRightEdgesKeepsAlreadySeparatedIntervalsInPlace() {
let intervals: [ClosedRange<CGFloat>] = [0...12, 20...32, 40...52]
let rightEdges = ShortcutHintHorizontalPlanner.assignRightEdges(for: intervals, minSpacing: 4)
XCTAssertEqual(rightEdges, [12, 32, 52])
}
}
final class LastSurfaceCloseShortcutSettingsTests: XCTestCase {
func testDefaultClosesWorkspace() {
let suiteName = "LastSurfaceCloseShortcutSettingsTests.Default.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
XCTAssertTrue(LastSurfaceCloseShortcutSettings.closesWorkspace(defaults: defaults))
}
func testStoredTrueClosesWorkspace() {
let suiteName = "LastSurfaceCloseShortcutSettingsTests.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: LastSurfaceCloseShortcutSettings.key)
XCTAssertTrue(LastSurfaceCloseShortcutSettings.closesWorkspace(defaults: defaults))
}
func testStoredFalseKeepsWorkspaceOpen() {
let suiteName = "LastSurfaceCloseShortcutSettingsTests.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: LastSurfaceCloseShortcutSettings.key)
XCTAssertFalse(LastSurfaceCloseShortcutSettings.closesWorkspace(defaults: defaults))
}
}
final class AppearanceSettingsTests: XCTestCase {
func testResolvedModeDefaultsToSystemWhenUnset() {
let suiteName = "AppearanceSettingsTests.Default.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.removeObject(forKey: AppearanceSettings.appearanceModeKey)
let resolved = AppearanceSettings.resolvedMode(defaults: defaults)
XCTAssertEqual(resolved, .system)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue)
}
}
final class QuitWarningSettingsTests: XCTestCase {
func testDefaultWarnBeforeQuitIsEnabledWhenUnset() {
let suiteName = "QuitWarningSettingsTests.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: QuitWarningSettings.warnBeforeQuitKey)
XCTAssertTrue(QuitWarningSettings.isEnabled(defaults: defaults))
}
func testStoredPreferenceOverridesDefault() {
let suiteName = "QuitWarningSettingsTests.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: QuitWarningSettings.warnBeforeQuitKey)
XCTAssertFalse(QuitWarningSettings.isEnabled(defaults: defaults))
defaults.set(true, forKey: QuitWarningSettings.warnBeforeQuitKey)
XCTAssertTrue(QuitWarningSettings.isEnabled(defaults: defaults))
}
}
final class UpdateChannelSettingsTests: XCTestCase {
func testResolvedFeedFallsBackWhenInfoFeedMissing() {
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: nil)
XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL)
XCTAssertFalse(resolved.isNightly)
XCTAssertTrue(resolved.usedFallback)
}
func testResolvedFeedFallsBackWhenInfoFeedEmpty() {
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: "")
XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL)
XCTAssertFalse(resolved.isNightly)
XCTAssertTrue(resolved.usedFallback)
}
func testResolvedFeedUsesInfoFeedForStableChannel() {
let infoFeed = "https://example.com/custom/appcast.xml"
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: infoFeed)
XCTAssertEqual(resolved.url, infoFeed)
XCTAssertFalse(resolved.isNightly)
XCTAssertFalse(resolved.usedFallback)
}
func testResolvedFeedDetectsNightlyFromInfoFeedURL() {
let resolved = UpdateFeedResolver.resolvedFeedURLString(
infoFeedURL: "https://example.com/nightly/appcast.xml"
)
XCTAssertEqual(resolved.url, "https://example.com/nightly/appcast.xml")
XCTAssertTrue(resolved.isNightly)
XCTAssertFalse(resolved.usedFallback)
}
}
final class UpdateSettingsTests: XCTestCase {
func testApplyEnablesAutomaticChecksAndDailySchedule() {
let defaults = makeDefaults()
UpdateSettings.apply(to: defaults)
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.automaticChecksKey))
XCTAssertEqual(defaults.double(forKey: UpdateSettings.scheduledCheckIntervalKey), UpdateSettings.scheduledCheckInterval)
XCTAssertFalse(defaults.bool(forKey: UpdateSettings.automaticallyUpdateKey))
XCTAssertFalse(defaults.bool(forKey: UpdateSettings.sendProfileInfoKey))
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.migrationKey))
}
func testApplyRepairsLegacyDisabledAutomaticChecksOnce() {
let defaults = makeDefaults()
defaults.set(false, forKey: UpdateSettings.automaticChecksKey)
defaults.set(0, forKey: UpdateSettings.scheduledCheckIntervalKey)
defaults.set(true, forKey: UpdateSettings.automaticallyUpdateKey)
UpdateSettings.apply(to: defaults)
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.automaticChecksKey))
XCTAssertEqual(defaults.double(forKey: UpdateSettings.scheduledCheckIntervalKey), UpdateSettings.scheduledCheckInterval)
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.automaticallyUpdateKey))
defaults.set(false, forKey: UpdateSettings.automaticChecksKey)
UpdateSettings.apply(to: defaults)
XCTAssertFalse(defaults.bool(forKey: UpdateSettings.automaticChecksKey))
}
private func makeDefaults() -> UserDefaults {
let suiteName = "UpdateSettingsTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
fatalError("Failed to create isolated UserDefaults suite")
}
defaults.removePersistentDomain(forName: suiteName)
return defaults
}
}
@MainActor
final class CommandPaletteOverlayPromotionPolicyTests: XCTestCase {
func testShouldPromoteWhenBecomingVisible() {
XCTAssertTrue(
CommandPaletteOverlayPromotionPolicy.shouldPromote(
previouslyVisible: false,
isVisible: true
)
)
}
func testShouldNotPromoteWhenAlreadyVisible() {
XCTAssertFalse(
CommandPaletteOverlayPromotionPolicy.shouldPromote(
previouslyVisible: true,
isVisible: true
)
)
}
func testShouldNotPromoteWhenHidden() {
XCTAssertFalse(
CommandPaletteOverlayPromotionPolicy.shouldPromote(
previouslyVisible: true,
isVisible: false
)
)
XCTAssertFalse(
CommandPaletteOverlayPromotionPolicy.shouldPromote(
previouslyVisible: false,
isVisible: false
)
)
}
}

View file

@ -0,0 +1,941 @@
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",
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",
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",
fallbackBranch: SidebarGitBranchState(branch: "main", isDirty: false)
)
XCTAssertEqual(
rows,
[SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: false, directory: "/repo/default")]
)
}
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"
)
}
}

View file

@ -0,0 +1,976 @@
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 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)
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff