File system cleanup
This commit is contained in:
248
CommandNotch/CommandNotchTests/NotchOrchestratorTests.swift
Normal file
248
CommandNotch/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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user