249 lines
8.5 KiB
Swift
249 lines
8.5 KiB
Swift
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
|
|
}
|
|
}
|