Yep. AI rewrote the whole thing.
This commit is contained in:
42
Downterm/CommandNotchTests/AppSettingsControllerTests.swift
Normal file
42
Downterm/CommandNotchTests/AppSettingsControllerTests.swift
Normal 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
|
||||
}
|
||||
}
|
||||
38
Downterm/CommandNotchTests/AppSettingsStoreTests.swift
Normal file
38
Downterm/CommandNotchTests/AppSettingsStoreTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
248
Downterm/CommandNotchTests/NotchOrchestratorTests.swift
Normal file
248
Downterm/CommandNotchTests/NotchOrchestratorTests.swift
Normal 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
|
||||
}
|
||||
}
|
||||
67
Downterm/CommandNotchTests/ScreenContextTests.swift
Normal file
67
Downterm/CommandNotchTests/ScreenContextTests.swift
Normal 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
|
||||
}
|
||||
}
|
||||
319
Downterm/CommandNotchTests/ScreenRegistryTests.swift
Normal file
319
Downterm/CommandNotchTests/ScreenRegistryTests.swift
Normal 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.")
|
||||
}
|
||||
}
|
||||
32
Downterm/CommandNotchTests/WindowFrameCalculatorTests.swift
Normal file
32
Downterm/CommandNotchTests/WindowFrameCalculatorTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
122
Downterm/CommandNotchTests/WorkspaceRegistryTests.swift
Normal file
122
Downterm/CommandNotchTests/WorkspaceRegistryTests.swift
Normal file
@@ -0,0 +1,122 @@
|
||||
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"])
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
36
Downterm/CommandNotchTests/WorkspaceStoreTests.swift
Normal file
36
Downterm/CommandNotchTests/WorkspaceStoreTests.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
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"),
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user