mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
BREAKING CHANGE: Package name changed due to name collision with existing rensbreur/SwiftTUI package. Changes: - Rename package from SwiftTUI to TUIKit in Package.swift - Rename Sources/SwiftTUI to Sources/TUIKit - Rename Sources/SwiftTUIExample to Sources/TUIKitExample - Rename Tests/SwiftTUITests to Tests/TUIKitTests - Rename SwiftTUI.swift to TUIKit.swift - Update all imports: import SwiftTUI -> import TUIKit - Update all code references: SwiftTUI.renderToBuffer -> TUIKit.renderToBuffer - Update documentation comments - Rename swiftTUIVersion to tuiKitVersion All 181 tests passing.
428 lines
12 KiB
Swift
428 lines
12 KiB
Swift
//
|
|
// FocusTests.swift
|
|
// TUIKit
|
|
//
|
|
// Tests for the focus management system.
|
|
//
|
|
|
|
import Foundation
|
|
import Testing
|
|
@testable import TUIKit
|
|
|
|
// MARK: - Mock Focusable
|
|
|
|
/// A mock focusable element for testing.
|
|
final class MockFocusable: Focusable {
|
|
let focusID: String
|
|
var canBeFocused: Bool
|
|
var focusReceivedCount = 0
|
|
var focusLostCount = 0
|
|
var lastKeyEvent: KeyEvent?
|
|
var shouldConsumeEvents: Bool
|
|
|
|
init(id: String, canBeFocused: Bool = true, shouldConsumeEvents: Bool = false) {
|
|
self.focusID = id
|
|
self.canBeFocused = canBeFocused
|
|
self.shouldConsumeEvents = shouldConsumeEvents
|
|
}
|
|
|
|
func onFocusReceived() {
|
|
focusReceivedCount += 1
|
|
}
|
|
|
|
func onFocusLost() {
|
|
focusLostCount += 1
|
|
}
|
|
|
|
func handleKeyEvent(_ event: KeyEvent) -> Bool {
|
|
lastKeyEvent = event
|
|
return shouldConsumeEvents
|
|
}
|
|
}
|
|
|
|
// MARK: - Focus Manager Tests
|
|
|
|
@Suite("Focus Manager Tests")
|
|
struct FocusManagerTests {
|
|
|
|
@Test("FocusManager can be instantiated")
|
|
func focusManagerInstantiation() {
|
|
let manager1 = FocusManager()
|
|
let manager2 = FocusManager()
|
|
// Each instance is independent
|
|
#expect(manager1 !== manager2)
|
|
}
|
|
|
|
@Test("Register focusable element")
|
|
func registerFocusable() {
|
|
let manager = FocusManager()
|
|
|
|
let element = MockFocusable(id: "test-element")
|
|
manager.register(element)
|
|
|
|
// First registered element should be auto-focused
|
|
#expect(manager.isFocused(element))
|
|
#expect(manager.currentFocusedID == "test-element")
|
|
}
|
|
|
|
@Test("Register multiple elements")
|
|
func registerMultipleElements() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "element-1")
|
|
let element2 = MockFocusable(id: "element-2")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
|
|
// First element should be focused
|
|
#expect(manager.isFocused(element1))
|
|
#expect(!manager.isFocused(element2))
|
|
}
|
|
|
|
@Test("Avoid duplicate registration")
|
|
func avoidDuplicates() {
|
|
let manager = FocusManager()
|
|
|
|
let element = MockFocusable(id: "unique-element")
|
|
manager.register(element)
|
|
manager.register(element)
|
|
|
|
// Should still work without issues
|
|
#expect(manager.isFocused(element))
|
|
}
|
|
|
|
@Test("Unregister focusable element")
|
|
func unregisterFocusable() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "elem-1")
|
|
let element2 = MockFocusable(id: "elem-2")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
manager.unregister(element1)
|
|
|
|
// element2 should now be focused (focusNext called)
|
|
#expect(!manager.isFocused(element1))
|
|
}
|
|
|
|
@Test("Clear all focusables")
|
|
func clearAll() {
|
|
let manager = FocusManager()
|
|
|
|
let element = MockFocusable(id: "to-clear")
|
|
manager.register(element)
|
|
manager.clear()
|
|
|
|
#expect(manager.currentFocusedID == nil)
|
|
#expect(manager.currentFocused == nil)
|
|
}
|
|
|
|
@Test("Focus specific element")
|
|
func focusSpecific() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "first")
|
|
let element2 = MockFocusable(id: "second")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
manager.focus(element2)
|
|
|
|
#expect(manager.isFocused(element2))
|
|
#expect(!manager.isFocused(element1))
|
|
}
|
|
|
|
@Test("Focus by ID")
|
|
func focusByID() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "id-a")
|
|
let element2 = MockFocusable(id: "id-b")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
manager.focus(id: "id-b")
|
|
|
|
#expect(manager.isFocused(id: "id-b"))
|
|
#expect(!manager.isFocused(id: "id-a"))
|
|
}
|
|
|
|
@Test("Focus next wraps around")
|
|
func focusNextWrapsAround() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "nav-1")
|
|
let element2 = MockFocusable(id: "nav-2")
|
|
let element3 = MockFocusable(id: "nav-3")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
manager.register(element3)
|
|
|
|
// Start at element1
|
|
#expect(manager.isFocused(element1))
|
|
|
|
manager.focusNext()
|
|
#expect(manager.isFocused(element2))
|
|
|
|
manager.focusNext()
|
|
#expect(manager.isFocused(element3))
|
|
|
|
manager.focusNext() // Should wrap to element1
|
|
#expect(manager.isFocused(element1))
|
|
}
|
|
|
|
@Test("Focus previous wraps around")
|
|
func focusPreviousWrapsAround() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "prev-1")
|
|
let element2 = MockFocusable(id: "prev-2")
|
|
let element3 = MockFocusable(id: "prev-3")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
manager.register(element3)
|
|
|
|
// Explicitly start at element1
|
|
manager.focus(element1)
|
|
#expect(manager.isFocused(element1))
|
|
|
|
manager.focusPrevious() // Should wrap to element3
|
|
#expect(manager.isFocused(element3))
|
|
|
|
manager.focusPrevious()
|
|
#expect(manager.isFocused(element2))
|
|
|
|
manager.focusPrevious()
|
|
#expect(manager.isFocused(element1))
|
|
}
|
|
|
|
@Test("Skip non-focusable elements")
|
|
func skipNonFocusable() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "focusable-1")
|
|
let element2 = MockFocusable(id: "disabled", canBeFocused: false)
|
|
let element3 = MockFocusable(id: "focusable-2")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
manager.register(element3)
|
|
|
|
// Start at element1
|
|
manager.focusNext()
|
|
// Should skip element2 and go to element3
|
|
#expect(manager.isFocused(element3))
|
|
}
|
|
|
|
@Test("Cannot focus disabled element")
|
|
func cannotFocusDisabled() {
|
|
let manager = FocusManager()
|
|
|
|
let disabled = MockFocusable(id: "disabled", canBeFocused: false)
|
|
manager.register(disabled)
|
|
|
|
// Should not be focused
|
|
#expect(!manager.isFocused(disabled))
|
|
#expect(manager.currentFocusedID == nil)
|
|
}
|
|
|
|
@Test("Focus callbacks are called")
|
|
func focusCallbacks() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "callback-1")
|
|
let element2 = MockFocusable(id: "callback-2")
|
|
|
|
// Register element1 - it should be auto-focused since manager was just created
|
|
manager.register(element1)
|
|
// Note: focusReceivedCount should be 1 after registration auto-focus
|
|
#expect(element1.focusReceivedCount == 1, "Element1 should receive focus when registered as first element")
|
|
|
|
manager.register(element2)
|
|
// Explicitly focus element2
|
|
manager.focus(element2)
|
|
|
|
// element1 should have lost focus, element2 should have received it
|
|
#expect(element1.focusLostCount == 1, "Element1 should lose focus when element2 is focused")
|
|
#expect(element2.focusReceivedCount == 1, "Element2 should receive focus when explicitly focused")
|
|
}
|
|
|
|
@Test("Tab key moves focus next")
|
|
func tabKeyMovesFocusNext() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "tab-1")
|
|
let element2 = MockFocusable(id: "tab-2")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
|
|
let tabEvent = KeyEvent(key: .tab, ctrl: false, alt: false, shift: false)
|
|
let handled = manager.dispatchKeyEvent(tabEvent)
|
|
|
|
#expect(handled)
|
|
#expect(manager.isFocused(element2))
|
|
}
|
|
|
|
@Test("Shift+Tab moves focus previous")
|
|
func shiftTabMovesFocusPrevious() {
|
|
let manager = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "shift-1")
|
|
let element2 = MockFocusable(id: "shift-2")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
manager.focus(element2)
|
|
|
|
let shiftTabEvent = KeyEvent(key: .tab, ctrl: false, alt: false, shift: true)
|
|
let handled = manager.dispatchKeyEvent(shiftTabEvent)
|
|
|
|
#expect(handled)
|
|
#expect(manager.isFocused(element1))
|
|
}
|
|
|
|
@Test("Key events dispatched to focused element")
|
|
func keyEventsDispatched() {
|
|
let manager = FocusManager()
|
|
|
|
let element = MockFocusable(id: "dispatch-test", shouldConsumeEvents: true)
|
|
manager.register(element)
|
|
|
|
let event = KeyEvent(key: .enter, ctrl: false, alt: false, shift: false)
|
|
let handled = manager.dispatchKeyEvent(event)
|
|
|
|
#expect(handled)
|
|
#expect(element.lastKeyEvent?.key == .enter)
|
|
}
|
|
|
|
@Test("onFocusChange callback triggered")
|
|
func onFocusChangeCallback() {
|
|
let manager = FocusManager()
|
|
|
|
var callbackCount = 0
|
|
manager.onFocusChange = {
|
|
callbackCount += 1
|
|
}
|
|
|
|
let element = MockFocusable(id: "callback-test")
|
|
manager.register(element)
|
|
|
|
#expect(callbackCount == 1)
|
|
|
|
// Cleanup
|
|
manager.onFocusChange = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Focus State Tests
|
|
|
|
@Suite("Focus State Tests")
|
|
struct FocusStateTests {
|
|
|
|
@Test("FocusState can be created with ID")
|
|
func focusStateCreation() {
|
|
let state = FocusState(id: "my-focus-id")
|
|
#expect(state.id == "my-focus-id")
|
|
}
|
|
|
|
@Test("FocusState generates UUID if no ID provided")
|
|
func focusStateGeneratesUUID() {
|
|
let state = FocusState()
|
|
#expect(!state.id.isEmpty)
|
|
// UUID format check
|
|
#expect(state.id.contains("-"))
|
|
}
|
|
|
|
@Test("FocusState isFocused reflects environment focus manager state")
|
|
func focusStateIsFocused() {
|
|
// Set up a focus manager in the environment
|
|
let manager = FocusManager()
|
|
var environment = EnvironmentValues()
|
|
environment.focusManager = manager
|
|
EnvironmentStorage.shared.environment = environment
|
|
|
|
let state = FocusState(id: "state-test")
|
|
let element = MockFocusable(id: "state-test")
|
|
|
|
manager.register(element)
|
|
|
|
// The element is focused, so state should report focused
|
|
#expect(state.isFocused)
|
|
|
|
// Cleanup
|
|
EnvironmentStorage.shared.reset()
|
|
}
|
|
|
|
@Test("FocusState requestFocus works")
|
|
func focusStateRequestFocus() {
|
|
// Set up a focus manager in the environment
|
|
let manager = FocusManager()
|
|
var environment = EnvironmentValues()
|
|
environment.focusManager = manager
|
|
EnvironmentStorage.shared.environment = environment
|
|
|
|
let element1 = MockFocusable(id: "req-1")
|
|
let element2 = MockFocusable(id: "req-2")
|
|
|
|
manager.register(element1)
|
|
manager.register(element2)
|
|
|
|
let state = FocusState(id: "req-2")
|
|
state.requestFocus()
|
|
|
|
#expect(manager.isFocused(id: "req-2"))
|
|
|
|
// Cleanup
|
|
EnvironmentStorage.shared.reset()
|
|
}
|
|
}
|
|
|
|
// MARK: - Focus Manager Environment Tests
|
|
|
|
@Suite("Focus Manager Environment Tests")
|
|
struct FocusManagerEnvironmentTests {
|
|
|
|
@Test("FocusManager is accessible via environment key")
|
|
func focusManagerEnvironmentKey() {
|
|
let manager = FocusManager()
|
|
var environment = EnvironmentValues()
|
|
environment.focusManager = manager
|
|
|
|
#expect(environment.focusManager === manager)
|
|
}
|
|
|
|
@Test("Default FocusManager is provided if not set")
|
|
func defaultFocusManager() {
|
|
let environment = EnvironmentValues()
|
|
// Should return a default FocusManager instance
|
|
let defaultManager = environment.focusManager
|
|
#expect(defaultManager.currentFocusedID == nil)
|
|
}
|
|
|
|
@Test("Multiple tests can have independent FocusManagers")
|
|
func independentManagers() {
|
|
let manager1 = FocusManager()
|
|
let manager2 = FocusManager()
|
|
|
|
let element1 = MockFocusable(id: "test-1")
|
|
let element2 = MockFocusable(id: "test-2")
|
|
|
|
manager1.register(element1)
|
|
manager2.register(element2)
|
|
|
|
// Each manager should have its own focused element
|
|
#expect(manager1.currentFocusedID == "test-1")
|
|
#expect(manager2.currentFocusedID == "test-2")
|
|
|
|
// Clearing one shouldn't affect the other
|
|
manager1.clear()
|
|
#expect(manager1.currentFocusedID == nil)
|
|
#expect(manager2.currentFocusedID == "test-2")
|
|
}
|
|
}
|