File system cleanup

This commit is contained in:
2026-03-13 21:26:06 +11:00
parent 8ecb7d4382
commit cf3dba8fe4
83 changed files with 231 additions and 3 deletions

View File

@@ -0,0 +1,42 @@
import XCTest
@testable import CommandNotch
@MainActor
final class AppSettingsControllerTests: XCTestCase {
func testTerminalSessionConfigurationIncludesShellPath() {
let store = InMemoryAppSettingsStore()
var settings = AppSettings.default
settings.terminal.shellPath = "/opt/homebrew/bin/fish"
store.storedSettings = settings
let controller = AppSettingsController(store: store)
XCTAssertEqual(controller.terminalSessionConfiguration.shellPath, "/opt/homebrew/bin/fish")
}
func testTerminalSizePresetsDecodeFromTypedSettings() {
let store = InMemoryAppSettingsStore()
let presets = [
TerminalSizePreset(name: "Wide", width: 960, height: 420, hotkey: .cmdShiftDigit(4))
]
var settings = AppSettings.default
settings.terminal.sizePresetsJSON = TerminalSizePresetStore.encodePresets(presets)
store.storedSettings = settings
let controller = AppSettingsController(store: store)
XCTAssertEqual(controller.terminalSizePresets, presets)
}
}
private final class InMemoryAppSettingsStore: AppSettingsStoreType {
var storedSettings = AppSettings.default
func load() -> AppSettings {
storedSettings
}
func save(_ settings: AppSettings) {
storedSettings = settings
}
}

View File

@@ -0,0 +1,38 @@
import XCTest
@testable import CommandNotch
final class AppSettingsStoreTests: XCTestCase {
func testLoadReturnsDefaultValuesWhenStoreIsEmpty() {
let defaults = UserDefaults(suiteName: #function)!
defaults.removePersistentDomain(forName: #function)
defer { defaults.removePersistentDomain(forName: #function) }
let store = UserDefaultsAppSettingsStore(defaults: defaults)
XCTAssertEqual(store.load(), .default)
}
func testSaveRoundTripsSettings() {
let defaults = UserDefaults(suiteName: #function)!
defaults.removePersistentDomain(forName: #function)
defer { defaults.removePersistentDomain(forName: #function) }
let store = UserDefaultsAppSettingsStore(defaults: defaults)
var settings = AppSettings.default
settings.display.showMenuBarIcon = false
settings.display.showOnAllDisplays = false
settings.display.openWidth = 900
settings.behavior.minimumHoverDuration = 0.65
settings.appearance.blurRadius = 4.5
settings.terminal.fontSize = 16
settings.terminal.themeRawValue = TerminalTheme.dracula.rawValue
settings.terminal.sizePresetsJSON = TerminalSizePresetStore.encodePresets([
TerminalSizePreset(name: "Wide", width: 960, height: 420, hotkey: .cmdShiftDigit(4))
])
settings.hotkeys.toggle = .cmdD
store.save(settings)
XCTAssertEqual(store.load(), settings)
}
}

View File

@@ -0,0 +1,248 @@
import XCTest
import Combine
@testable import CommandNotch
@MainActor
final class NotchOrchestratorTests: XCTestCase {
func testHoverOpenSchedulesOpenAfterDelay() {
let screenID = "screen-a"
let screen = makeScreenContext(screenID: screenID)
let registry = TestScreenRegistry(activeScreenID: screenID, screens: [screen])
let host = TestNotchPresentationHost()
let scheduler = TestScheduler()
let orchestrator = NotchOrchestrator(
screenRegistry: registry,
host: host,
settingsController: makeSettingsController(),
scheduler: scheduler
)
orchestrator.handleHoverChange(true, for: screenID)
XCTAssertEqual(screen.notchState, .closed)
scheduler.runScheduledActions()
XCTAssertEqual(screen.notchState, .open)
XCTAssertEqual(host.openedScreenIDs, [screenID])
}
func testHoverExitCancelsPendingOpen() {
let screenID = "screen-a"
let screen = makeScreenContext(screenID: screenID)
let registry = TestScreenRegistry(activeScreenID: screenID, screens: [screen])
let host = TestNotchPresentationHost()
let scheduler = TestScheduler()
let orchestrator = NotchOrchestrator(
screenRegistry: registry,
host: host,
settingsController: makeSettingsController(),
scheduler: scheduler
)
orchestrator.handleHoverChange(true, for: screenID)
orchestrator.handleHoverChange(false, for: screenID)
scheduler.runScheduledActions()
XCTAssertEqual(screen.notchState, .closed)
XCTAssertTrue(host.openedScreenIDs.isEmpty)
}
func testCloseWhileHoveringSuppressesReopenUntilHoverExit() {
let screenID = "screen-a"
let screen = makeScreenContext(screenID: screenID)
let registry = TestScreenRegistry(activeScreenID: screenID, screens: [screen])
let host = TestNotchPresentationHost()
let scheduler = TestScheduler()
let orchestrator = NotchOrchestrator(
screenRegistry: registry,
host: host,
settingsController: makeSettingsController(),
scheduler: scheduler
)
orchestrator.handleHoverChange(true, for: screenID)
scheduler.runScheduledActions()
XCTAssertEqual(screen.notchState, .open)
orchestrator.handleHoverChange(true, for: screenID)
orchestrator.close(screenID: screenID)
scheduler.runScheduledActions()
XCTAssertEqual(screen.notchState, .closed)
XCTAssertFalse(screen.isCloseTransitionActive)
XCTAssertTrue(screen.suppressHoverOpenUntilHoverExit)
XCTAssertEqual(host.closedScreenIDs, [screenID])
scheduler.runScheduledActions()
XCTAssertEqual(screen.notchState, .closed)
XCTAssertEqual(host.openedScreenIDs, [screenID])
orchestrator.handleHoverChange(false, for: screenID)
XCTAssertFalse(screen.suppressHoverOpenUntilHoverExit)
}
func testOpeningSharedWorkspaceOnAnotherScreenClosesPreviousPresenter() {
let workspaceID = UUID()
let firstScreen = makeScreenContext(screenID: "screen-a", workspaceID: workspaceID)
let secondScreen = makeScreenContext(screenID: "screen-b", workspaceID: workspaceID)
let registry = TestScreenRegistry(activeScreenID: "screen-b", screens: [firstScreen, secondScreen])
let host = TestNotchPresentationHost()
let orchestrator = NotchOrchestrator(
screenRegistry: registry,
host: host,
settingsController: makeSettingsController(),
scheduler: TestScheduler()
)
orchestrator.open(screenID: "screen-a")
orchestrator.open(screenID: "screen-b")
XCTAssertEqual(firstScreen.notchState, .closed)
XCTAssertEqual(secondScreen.notchState, .open)
XCTAssertEqual(host.closedScreenIDs, ["screen-a"])
XCTAssertEqual(registry.presentingScreenID(for: workspaceID), "screen-b")
}
func testOpeningDifferentWorkspaceDoesNotCloseOtherOpenScreen() {
let firstScreen = makeScreenContext(screenID: "screen-a", workspaceID: UUID())
let secondScreen = makeScreenContext(screenID: "screen-b", workspaceID: UUID())
let registry = TestScreenRegistry(activeScreenID: "screen-b", screens: [firstScreen, secondScreen])
let host = TestNotchPresentationHost()
let orchestrator = NotchOrchestrator(
screenRegistry: registry,
host: host,
settingsController: makeSettingsController(),
scheduler: TestScheduler()
)
orchestrator.open(screenID: "screen-a")
orchestrator.open(screenID: "screen-b")
XCTAssertEqual(firstScreen.notchState, .open)
XCTAssertEqual(secondScreen.notchState, .open)
XCTAssertTrue(host.closedScreenIDs.isEmpty)
XCTAssertEqual(registry.presentingScreenID(for: firstScreen.workspaceID), "screen-a")
XCTAssertEqual(registry.presentingScreenID(for: secondScreen.workspaceID), "screen-b")
}
private func makeScreenContext(screenID: ScreenID, workspaceID: WorkspaceID = UUID()) -> ScreenContext {
ScreenContext(
id: screenID,
workspaceID: workspaceID,
settingsController: makeSettingsController(),
screenProvider: { _ in nil }
)
}
private func makeSettingsController() -> AppSettingsController {
let store = TestOrchestratorSettingsStore()
var settings = AppSettings.default
settings.behavior.openNotchOnHover = true
settings.behavior.minimumHoverDuration = 0.3
store.storedSettings = settings
return AppSettingsController(store: store)
}
}
@MainActor
private final class TestScreenRegistry: ScreenRegistryType {
private let activeID: ScreenID
private var screensByID: [ScreenID: ScreenContext]
private var workspacePresenters: [WorkspaceID: ScreenID] = [:]
init(activeScreenID: ScreenID, screens: [ScreenContext]) {
self.activeID = activeScreenID
self.screensByID = Dictionary(uniqueKeysWithValues: screens.map { ($0.id, $0) })
}
func allScreens() -> [ScreenContext] {
Array(screensByID.values)
}
func screenContext(for id: ScreenID) -> ScreenContext? {
screensByID[id]
}
func activeScreenID() -> ScreenID? {
activeID
}
func presentingScreenID(for workspaceID: WorkspaceID) -> ScreenID? {
workspacePresenters[workspaceID]
}
func claimWorkspacePresentation(for screenID: ScreenID) -> ScreenID? {
guard let workspaceID = screensByID[screenID]?.workspaceID else { return nil }
let previous = workspacePresenters[workspaceID]
workspacePresenters[workspaceID] = screenID
return previous == screenID ? nil : previous
}
func releaseWorkspacePresentation(for screenID: ScreenID) {
workspacePresenters = workspacePresenters.filter { $0.value != screenID }
}
}
@MainActor
private final class TestNotchPresentationHost: NotchPresentationHost {
var openedScreenIDs: [ScreenID] = []
var closedScreenIDs: [ScreenID] = []
func canPresentNotch(for screenID: ScreenID) -> Bool {
true
}
func performOpenPresentation(for screenID: ScreenID) {
openedScreenIDs.append(screenID)
}
func performClosePresentation(for screenID: ScreenID) {
closedScreenIDs.append(screenID)
}
}
private final class TestScheduler: SchedulerType {
private final class ScheduledAction {
let action: @MainActor () -> Void
var isCancelled = false
init(action: @escaping @MainActor () -> Void) {
self.action = action
}
}
private var scheduledActions: [ScheduledAction] = []
@MainActor
func schedule(after interval: TimeInterval, action: @escaping @MainActor () -> Void) -> AnyCancellable {
let scheduledAction = ScheduledAction(action: action)
scheduledActions.append(scheduledAction)
return AnyCancellable {
scheduledAction.isCancelled = true
}
}
@MainActor
func runScheduledActions() {
let actions = scheduledActions
scheduledActions.removeAll()
for scheduledAction in actions where !scheduledAction.isCancelled {
scheduledAction.action()
}
}
}
private final class TestOrchestratorSettingsStore: AppSettingsStoreType {
var storedSettings = AppSettings.default
func load() -> AppSettings {
storedSettings
}
func save(_ settings: AppSettings) {
storedSettings = settings
}
}

View File

@@ -0,0 +1,67 @@
import XCTest
@testable import CommandNotch
@MainActor
final class ScreenContextTests: XCTestCase {
func testInteractiveResizeDefersPersistingSettingsUntilResizeEnds() {
let store = ScreenContextTestSettingsStore()
var settings = AppSettings.default
settings.display.openWidth = 640
settings.display.openHeight = 350
store.storedSettings = settings
let controller = AppSettingsController(store: store)
let screen = ScreenContext(
id: "screen-a",
workspaceID: UUID(),
settingsController: controller,
screenProvider: { _ in nil }
)
screen.open()
screen.beginInteractiveResize()
screen.resizeOpenNotch(to: CGSize(width: 800, height: 420))
XCTAssertEqual(screen.notchSize.width, 800)
XCTAssertEqual(screen.notchSize.height, 420)
XCTAssertEqual(controller.settings.display.openWidth, 640)
XCTAssertEqual(controller.settings.display.openHeight, 350)
screen.endInteractiveResize()
XCTAssertEqual(controller.settings.display.openWidth, 800)
XCTAssertEqual(controller.settings.display.openHeight, 420)
XCTAssertEqual(store.storedSettings.display.openWidth, 800)
XCTAssertEqual(store.storedSettings.display.openHeight, 420)
}
func testFocusLossAutoCloseSuppressionCanBeToggled() {
let controller = AppSettingsController(store: ScreenContextTestSettingsStore())
let screen = ScreenContext(
id: "screen-a",
workspaceID: UUID(),
settingsController: controller,
screenProvider: { _ in nil }
)
XCTAssertFalse(screen.suppressCloseOnFocusLoss)
screen.setCloseOnFocusLossSuppressed(true)
XCTAssertTrue(screen.suppressCloseOnFocusLoss)
screen.setCloseOnFocusLossSuppressed(false)
XCTAssertFalse(screen.suppressCloseOnFocusLoss)
}
}
private final class ScreenContextTestSettingsStore: AppSettingsStoreType {
var storedSettings = AppSettings.default
func load() -> AppSettings {
storedSettings
}
func save(_ settings: AppSettings) {
storedSettings = settings
}
}

View File

@@ -0,0 +1,319 @@
import XCTest
@testable import CommandNotch
@MainActor
final class ScreenRegistryTests: XCTestCase {
func testRefreshCreatesContextsForConnectedScreensUsingDefaultWorkspace() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
connectedScreenIDsProvider: { ["screen-a", "screen-b"] },
activeScreenIDProvider: { "screen-b" },
screenLookup: { _ in nil }
)
XCTAssertEqual(registry.allScreens().map(\.id), ["screen-a", "screen-b"])
XCTAssertEqual(
registry.allScreens().map(\.workspaceID),
[workspaceRegistry.defaultWorkspaceID, workspaceRegistry.defaultWorkspaceID]
)
XCTAssertEqual(registry.activeScreenID(), "screen-b")
}
func testAssignWorkspaceUpdatesContextAndSurvivesReconnect() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
var connectedScreenIDs = ["screen-a"]
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
connectedScreenIDsProvider: { connectedScreenIDs },
activeScreenIDProvider: { connectedScreenIDs.first },
screenLookup: { _ in nil }
)
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
registry.assignWorkspace(docsWorkspaceID, to: "screen-a")
XCTAssertEqual(registry.screenContext(for: "screen-a")?.workspaceID, docsWorkspaceID)
connectedScreenIDs = []
registry.refreshConnectedScreens()
XCTAssertNil(registry.screenContext(for: "screen-a"))
connectedScreenIDs = ["screen-a"]
registry.refreshConnectedScreens()
XCTAssertEqual(registry.screenContext(for: "screen-a")?.workspaceID, docsWorkspaceID)
}
func testDeletedWorkspaceAssignmentFallsBackToDefaultWorkspace() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let assignmentStore = InMemoryScreenAssignmentStore()
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
assignmentStore: assignmentStore,
connectedScreenIDsProvider: { ["screen-a"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
let reviewWorkspaceID = workspaceRegistry.createWorkspace(named: "Review")
registry.assignWorkspace(reviewWorkspaceID, to: "screen-a")
workspaceRegistry.deleteWorkspace(id: reviewWorkspaceID)
registry.refreshConnectedScreens()
XCTAssertEqual(
registry.screenContext(for: "screen-a")?.workspaceID,
workspaceRegistry.defaultWorkspaceID
)
XCTAssertEqual(
assignmentStore.savedAssignments["screen-a"],
workspaceRegistry.defaultWorkspaceID
)
}
func testRegistryLoadsPersistedAssignmentsFromStore() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
let assignmentStore = InMemoryScreenAssignmentStore()
assignmentStore.savedAssignments = ["screen-a": docsWorkspaceID]
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
assignmentStore: assignmentStore,
connectedScreenIDsProvider: { ["screen-a"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
XCTAssertEqual(registry.screenContext(for: "screen-a")?.workspaceID, docsWorkspaceID)
}
func testAssignWorkspacePersistsAssignments() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let assignmentStore = InMemoryScreenAssignmentStore()
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
assignmentStore: assignmentStore,
connectedScreenIDsProvider: { ["screen-a"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
let reviewWorkspaceID = workspaceRegistry.createWorkspace(named: "Review")
registry.assignWorkspace(reviewWorkspaceID, to: "screen-a")
XCTAssertEqual(assignmentStore.savedAssignments["screen-a"], reviewWorkspaceID)
}
func testWorkspaceControllerTracksAssignedWorkspace() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
connectedScreenIDsProvider: { ["screen-a"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
XCTAssertEqual(registry.workspaceController(for: "screen-a").id, workspaceRegistry.defaultWorkspaceID)
registry.assignWorkspace(docsWorkspaceID, to: "screen-a")
XCTAssertEqual(registry.workspaceController(for: "screen-a").id, docsWorkspaceID)
}
func testDeleteWorkspaceReassignsConnectedAndPersistedScreensToFallback() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
let reviewWorkspaceID = workspaceRegistry.createWorkspace(named: "Review")
let assignmentStore = InMemoryScreenAssignmentStore()
assignmentStore.savedAssignments = ["screen-a": docsWorkspaceID, "screen-b": docsWorkspaceID]
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
assignmentStore: assignmentStore,
connectedScreenIDsProvider: { ["screen-a"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
registry.assignWorkspace(docsWorkspaceID, to: "screen-a")
let fallbackWorkspaceID = registry.deleteWorkspace(
docsWorkspaceID,
preferredFallback: reviewWorkspaceID
)
XCTAssertEqual(fallbackWorkspaceID, reviewWorkspaceID)
XCTAssertEqual(registry.screenContext(for: "screen-a")?.workspaceID, reviewWorkspaceID)
XCTAssertEqual(assignmentStore.savedAssignments["screen-a"], reviewWorkspaceID)
XCTAssertEqual(assignmentStore.savedAssignments["screen-b"], reviewWorkspaceID)
}
func testAssignedScreenCountIncludesDisconnectedAssignments() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
let assignmentStore = InMemoryScreenAssignmentStore()
assignmentStore.savedAssignments = ["screen-a": docsWorkspaceID, "screen-b": docsWorkspaceID]
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
assignmentStore: assignmentStore,
connectedScreenIDsProvider: { ["screen-a"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
XCTAssertEqual(registry.assignedScreenCount(to: docsWorkspaceID), 2)
XCTAssertEqual(registry.assignedScreenIDs(to: docsWorkspaceID), ["screen-a", "screen-b"])
}
func testClaimWorkspacePresentationTracksPresenterPerWorkspace() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
connectedScreenIDsProvider: { ["screen-a", "screen-b"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
registry.assignWorkspace(docsWorkspaceID, to: "screen-a")
registry.assignWorkspace(docsWorkspaceID, to: "screen-b")
XCTAssertNil(registry.claimWorkspacePresentation(for: "screen-a"))
XCTAssertEqual(registry.presentingScreenID(for: docsWorkspaceID), "screen-a")
XCTAssertEqual(registry.claimWorkspacePresentation(for: "screen-b"), "screen-a")
XCTAssertEqual(registry.presentingScreenID(for: docsWorkspaceID), "screen-b")
registry.releaseWorkspacePresentation(for: "screen-b")
XCTAssertNil(registry.presentingScreenID(for: docsWorkspaceID))
}
func testAssignWorkspaceReleasesPreviousPresentationOwnership() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
let reviewWorkspaceID = workspaceRegistry.createWorkspace(named: "Review")
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
connectedScreenIDsProvider: { ["screen-a"] },
activeScreenIDProvider: { "screen-a" },
screenLookup: { _ in nil }
)
registry.assignWorkspace(docsWorkspaceID, to: "screen-a")
_ = registry.claimWorkspacePresentation(for: "screen-a")
XCTAssertEqual(registry.presentingScreenID(for: docsWorkspaceID), "screen-a")
registry.assignWorkspace(reviewWorkspaceID, to: "screen-a")
XCTAssertNil(registry.presentingScreenID(for: docsWorkspaceID))
XCTAssertEqual(registry.screenContext(for: "screen-a")?.workspaceID, reviewWorkspaceID)
}
func testConnectedScreenSummariesReflectActiveScreenAndAssignments() {
let workspaceRegistry = makeWorkspaceRegistry()
let settingsController = makeSettingsController()
let docsWorkspaceID = workspaceRegistry.createWorkspace(named: "Docs")
let registry = ScreenRegistry(
workspaceRegistry: workspaceRegistry,
settingsController: settingsController,
connectedScreenIDsProvider: { ["screen-a", "screen-b"] },
activeScreenIDProvider: { "screen-b" },
screenLookup: { _ in nil }
)
registry.assignWorkspace(docsWorkspaceID, to: "screen-a")
XCTAssertEqual(
registry.connectedScreenSummaries(),
[
ConnectedScreenSummary(
id: "screen-a",
displayName: "Screen 1",
isActive: false,
assignedWorkspaceID: docsWorkspaceID
),
ConnectedScreenSummary(
id: "screen-b",
displayName: "Screen 2",
isActive: true,
assignedWorkspaceID: workspaceRegistry.defaultWorkspaceID
)
]
)
}
private func makeWorkspaceRegistry() -> WorkspaceRegistry {
let settingsProvider = ScreenRegistryTestSettingsProvider()
let sessionFactory = ScreenRegistryUnusedTerminalSessionFactory()
return WorkspaceRegistry(
initialWorkspaces: [],
controllerFactory: { summary in
WorkspaceController(
summary: summary,
sessionFactory: sessionFactory,
settingsProvider: settingsProvider,
bootstrapDefaultTab: false
)
}
)
}
private func makeSettingsController() -> AppSettingsController {
AppSettingsController(store: TestAppSettingsStore())
}
}
private final class InMemoryScreenAssignmentStore: ScreenAssignmentStoreType {
var savedAssignments: [ScreenID: WorkspaceID] = [:]
func loadScreenAssignments() -> [ScreenID: WorkspaceID] {
savedAssignments
}
func saveScreenAssignments(_ assignments: [ScreenID: WorkspaceID]) {
savedAssignments = assignments
}
}
private final class TestAppSettingsStore: AppSettingsStoreType {
private var storedSettings = AppSettings.default
func load() -> AppSettings {
storedSettings
}
func save(_ settings: AppSettings) {
storedSettings = settings
}
}
private final class ScreenRegistryTestSettingsProvider: TerminalSessionConfigurationProviding {
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "")
let hotkeySettings = AppSettings.default.hotkeys
let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
}
private struct ScreenRegistryUnusedTerminalSessionFactory: TerminalSessionFactoryType {
@MainActor
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession {
fatalError("ScreenRegistryTests should not create live terminal sessions.")
}
}

View File

@@ -0,0 +1,57 @@
import AppKit
import Carbon.HIToolbox
import XCTest
import SwiftTerm
@testable import CommandNotch
final class TerminalCommandArrowBehaviorTests: XCTestCase {
func testCommandLeftUsesHomeSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_LeftArrow),
applicationCursor: false
)
XCTAssertEqual(sequence, EscapeSequences.moveHomeNormal)
}
func testCommandRightUsesApplicationEndSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_RightArrow),
applicationCursor: true
)
XCTAssertEqual(sequence, EscapeSequences.moveEndApp)
}
func testOptionLeftKeepsSwiftTermWordNavigationPath() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.option],
keyCode: UInt16(kVK_LeftArrow),
applicationCursor: false
)
XCTAssertNil(sequence)
}
func testCommandDeleteUsesLineKillSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_Delete),
applicationCursor: false
)
XCTAssertEqual(sequence, [0x15])
}
func testCommandLUsesClearScreenSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_ANSI_L),
applicationCursor: false
)
XCTAssertEqual(sequence, [0x0c])
}
}

View File

@@ -0,0 +1,32 @@
import XCTest
@testable import CommandNotch
final class WindowFrameCalculatorTests: XCTestCase {
func testClosedStateCentersWindowOnScreen() {
let frame = WindowFrameCalculator.targetFrame(
screenFrame: CGRect(x: 100, y: 50, width: 1600, height: 900),
currentWindowFrame: CGRect(x: 300, y: 0, width: 0, height: 0),
notchState: .closed,
contentSize: CGSize(width: 800, height: 300),
centerHorizontally: false
)
XCTAssertEqual(frame.origin.x, 480, accuracy: 0.001)
XCTAssertEqual(frame.origin.y, 630, accuracy: 0.001)
XCTAssertEqual(frame.size.width, 840, accuracy: 0.001)
XCTAssertEqual(frame.size.height, 320, accuracy: 0.001)
}
func testOpenStateClampsDraggedFrameWithinScreenBounds() {
let frame = WindowFrameCalculator.targetFrame(
screenFrame: CGRect(x: 0, y: 0, width: 1440, height: 900),
currentWindowFrame: CGRect(x: 1200, y: 0, width: 0, height: 0),
notchState: .open,
contentSize: CGSize(width: 900, height: 320),
centerHorizontally: false
)
XCTAssertEqual(frame.origin.x, 500, accuracy: 0.001)
XCTAssertEqual(frame.origin.y, 560, accuracy: 0.001)
}
}

View File

@@ -0,0 +1,146 @@
import XCTest
@testable import CommandNotch
@MainActor
final class WorkspaceRegistryTests: XCTestCase {
func testRegistryCreatesDefaultWorkspaceWhenEmpty() {
let registry = makeRegistry()
XCTAssertEqual(registry.allWorkspaceSummaries().count, 1)
XCTAssertEqual(registry.allWorkspaceSummaries().first?.name, "Main")
XCTAssertNotNil(registry.controller(for: registry.defaultWorkspaceID))
}
func testCreateRenameAndDeleteWorkspaceUpdatesSummaries() {
let registry = makeRegistry()
let createdID = registry.createWorkspace(named: "Docs")
XCTAssertEqual(registry.allWorkspaceSummaries().map(\.name), ["Main", "Docs"])
registry.renameWorkspace(id: createdID, to: "Review")
XCTAssertEqual(registry.allWorkspaceSummaries().map(\.name), ["Main", "Review"])
registry.deleteWorkspace(id: createdID)
XCTAssertEqual(registry.allWorkspaceSummaries().map(\.name), ["Main"])
}
func testDeletingLastWorkspaceIsIgnored() {
let registry = makeRegistry()
let onlyWorkspaceID = registry.defaultWorkspaceID
registry.deleteWorkspace(id: onlyWorkspaceID)
XCTAssertEqual(registry.allWorkspaceSummaries().count, 1)
XCTAssertEqual(registry.allWorkspaceSummaries().first?.id, onlyWorkspaceID)
}
func testDeletionFallbackPrefersRequestedWorkspaceWhenAvailable() {
let registry = makeRegistry()
let docsID = registry.createWorkspace(named: "Docs")
let reviewID = registry.createWorkspace(named: "Review")
let fallback = registry.deletionFallbackWorkspaceID(
forDeleting: docsID,
preferredFallback: reviewID
)
XCTAssertEqual(fallback, reviewID)
}
func testRegistryLoadsPersistedWorkspacesFromStore() {
let store = InMemoryWorkspaceStore()
let docsID = UUID(uuidString: "11111111-1111-1111-1111-111111111111")!
store.savedSummaries = [WorkspaceSummary(id: docsID, name: "Docs")]
let registry = makeRegistry(initialWorkspaces: nil, store: store)
XCTAssertEqual(registry.allWorkspaceSummaries().count, 1)
XCTAssertEqual(registry.allWorkspaceSummaries().first?.id, docsID)
XCTAssertEqual(registry.allWorkspaceSummaries().first?.name, "Docs")
XCTAssertEqual(registry.defaultWorkspaceID, docsID)
}
func testRegistryPersistsWorkspaceSummaryChanges() {
let store = InMemoryWorkspaceStore()
let registry = makeRegistry(store: store)
let createdID = registry.createWorkspace(named: "Docs")
XCTAssertEqual(store.savedSummaries.map(\.name), ["Main", "Docs"])
registry.renameWorkspace(id: createdID, to: "Review")
XCTAssertEqual(store.savedSummaries.map(\.name), ["Main", "Review"])
registry.deleteWorkspace(id: createdID)
XCTAssertEqual(store.savedSummaries.map(\.name), ["Main"])
}
func testUpdateWorkspaceHotkeyPersistsAndUpdatesSummary() {
let store = InMemoryWorkspaceStore()
let registry = makeRegistry(store: store)
let docsID = registry.createWorkspace(named: "Docs")
let hotkey = HotkeyBinding.cmdShiftDigit(4)
registry.updateWorkspaceHotkey(id: docsID, to: hotkey)
XCTAssertEqual(registry.summary(for: docsID)?.hotkey, hotkey)
XCTAssertEqual(store.savedSummaries.last?.hotkey, hotkey)
}
func testNextAndPreviousWorkspaceWrapAroundRegistryOrder() {
let registry = makeRegistry()
let mainID = registry.defaultWorkspaceID
let docsID = registry.createWorkspace(named: "Docs")
let reviewID = registry.createWorkspace(named: "Review")
XCTAssertEqual(registry.nextWorkspaceID(after: mainID), docsID)
XCTAssertEqual(registry.nextWorkspaceID(after: reviewID), mainID)
XCTAssertEqual(registry.previousWorkspaceID(before: mainID), reviewID)
XCTAssertEqual(registry.previousWorkspaceID(before: docsID), mainID)
}
private func makeRegistry(
initialWorkspaces: [WorkspaceSummary]? = [],
store: (any WorkspaceStoreType)? = nil
) -> WorkspaceRegistry {
let settingsProvider = TestSettingsProvider()
let sessionFactory = UnusedTerminalSessionFactory()
return WorkspaceRegistry(
initialWorkspaces: initialWorkspaces,
store: store,
controllerFactory: { summary in
WorkspaceController(
summary: summary,
sessionFactory: sessionFactory,
settingsProvider: settingsProvider,
bootstrapDefaultTab: false
)
}
)
}
}
private final class InMemoryWorkspaceStore: WorkspaceStoreType {
var savedSummaries: [WorkspaceSummary] = []
func loadWorkspaceSummaries() -> [WorkspaceSummary] {
savedSummaries
}
func saveWorkspaceSummaries(_ summaries: [WorkspaceSummary]) {
savedSummaries = summaries
}
}
private final class TestSettingsProvider: TerminalSessionConfigurationProviding {
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "")
let hotkeySettings = AppSettings.default.hotkeys
let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
}
private struct UnusedTerminalSessionFactory: TerminalSessionFactoryType {
@MainActor
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession {
fatalError("WorkspaceRegistryTests should not create live terminal sessions.")
}
}

View File

@@ -0,0 +1,40 @@
import XCTest
@testable import CommandNotch
final class WorkspaceStoreTests: XCTestCase {
func testWorkspaceStoreRoundTripsSummaries() {
let defaults = UserDefaults(suiteName: #function)!
defaults.removePersistentDomain(forName: #function)
defer { defaults.removePersistentDomain(forName: #function) }
let store = UserDefaultsWorkspaceStore(defaults: defaults)
let summaries = [
WorkspaceSummary(
id: UUID(uuidString: "11111111-1111-1111-1111-111111111111")!,
name: "Main",
hotkey: HotkeyBinding.cmdShiftDigit(4)
),
WorkspaceSummary(id: UUID(uuidString: "22222222-2222-2222-2222-222222222222")!, name: "Docs")
]
store.saveWorkspaceSummaries(summaries)
XCTAssertEqual(store.loadWorkspaceSummaries(), summaries)
}
func testScreenAssignmentStoreRoundTripsAssignments() {
let defaults = UserDefaults(suiteName: #function)!
defaults.removePersistentDomain(forName: #function)
defer { defaults.removePersistentDomain(forName: #function) }
let store = UserDefaultsScreenAssignmentStore(defaults: defaults)
let assignments: [ScreenID: WorkspaceID] = [
"screen-a": UUID(uuidString: "11111111-1111-1111-1111-111111111111")!,
"screen-b": UUID(uuidString: "22222222-2222-2222-2222-222222222222")!
]
store.saveScreenAssignments(assignments)
XCTAssertEqual(store.loadScreenAssignments(), assignments)
}
}