Files
TUIkit/Tests/TUIkitTests/ButtonTests.swift
T
phranck d0a66c28b3 Refactor: Consolidate ButtonHandler and ToggleHandler into ActionHandler (Phase 5)
- Create reusable ActionHandler for simple action-triggering views
- Button and Toggle now use ActionHandler instead of dedicated handlers
- RadioButtonGroupHandler unchanged (complex navigation logic)
- Update tests: ActionHandlerTests with custom trigger key test
- 590 tests passing
2026-02-07 16:28:20 +01:00

382 lines
11 KiB
Swift

// 🖥️ TUIKit — Terminal UI Kit for Swift
// ButtonTests.swift
//
// Created by LAYERED.work
// License: MIT
import Testing
@testable import TUIkit
// MARK: - Test Helpers
/// Creates a render context with a fresh FocusManager for isolated testing.
private func createTestContext(width: Int = 80, height: Int = 24) -> RenderContext {
let focusManager = FocusManager()
var environment = EnvironmentValues()
environment.focusManager = focusManager
return RenderContext(
availableWidth: width,
availableHeight: height,
environment: environment
)
}
// MARK: - Button Tests
@MainActor
@Suite("Button Tests", .serialized)
struct ButtonTests {
@Test("Button can be created with label and action")
func buttonCreation() {
var wasPressed = false
let button = Button("Click Me") {
wasPressed = true
}
#expect(button.label == "Click Me")
#expect(button.isDisabled == false)
button.action()
#expect(wasPressed == true)
}
@Test("Button disabled modifier")
func buttonDisabledModifier() {
let button = Button("Test") {}.disabled()
#expect(button.isDisabled == true)
let enabledButton = Button("Test") {}.disabled(false)
#expect(enabledButton.isDisabled == false)
}
@Test("Button generates unique focus ID by default")
func buttonGeneratesUniqueID() {
let button1 = Button("One") {}
let button2 = Button("Two") {}
#expect(button1.focusID != button2.focusID)
// UUID format check
#expect(button1.focusID.contains("-"))
}
@Test("Default button renders as single-line bracket style")
func defaultButtonRendersBrackets() {
let context = createTestContext()
let button = Button("OK") {}
let buffer = renderToBuffer(button, context: context)
// Bracket-style buttons are single line: [ OK ]
#expect(buffer.height == 1)
let allContent = buffer.lines.joined()
#expect(allContent.contains("OK"))
#expect(allContent.contains("["))
#expect(allContent.contains("]"))
}
@Test("Default button is single line height")
func defaultButtonSingleLine() {
let context = createTestContext()
let button = Button("Test", style: .default) {}
let buffer = renderToBuffer(button, context: context)
#expect(buffer.height == 1)
}
@Test("Plain button has single line without brackets")
func plainButtonSingleLine() {
let context = createTestContext()
let button = Button("Test", style: .plain) {}
let buffer = renderToBuffer(button, context: context)
#expect(buffer.height == 1)
// Check visible text (stripped of ANSI codes) has no brackets
let visibleContent = buffer.lines.joined().stripped
#expect(!visibleContent.contains("["))
#expect(!visibleContent.contains("]"))
}
@Test("Focused button has accent-colored brackets")
func focusedButtonHasAccentBrackets() {
let context = createTestContext()
let button = Button("Focus Me", focusID: "focused-button") {}
let buffer = renderToBuffer(button, context: context)
// First button is auto-focused — brackets should be styled (contain ANSI codes)
let allContent = buffer.lines.joined()
#expect(allContent.contains("["), "Button should have opening bracket")
#expect(allContent.contains("]"), "Button should have closing bracket")
#expect(allContent.contains("\u{1b}["), "Focused button should have ANSI styling")
}
@Test("Unfocused button has border-colored brackets")
func unfocusedButtonHasBorderBrackets() {
let context = createTestContext()
// Create two buttons — second one will be unfocused
let button1 = Button("First", focusID: "first") {}
let button2 = Button("Second", focusID: "second") {}
// Render first to register it (it gets focus)
_ = renderToBuffer(button1, context: context)
let buffer2 = renderToBuffer(button2, context: context)
// Second button is not focused — should still have brackets with styling
let allContent = buffer2.lines.joined()
#expect(allContent.contains("["), "Unfocused button should have opening bracket")
#expect(allContent.contains("]"), "Unfocused button should have closing bracket")
}
@Test("Destructive button uses palette error color, not hardcoded red")
func destructiveButtonUsesPaletteColor() {
let context = createTestContext()
let button = Button("Delete", style: .destructive) {}
let buffer = renderToBuffer(button, context: context)
let allContent = buffer.lines.joined()
#expect(allContent.contains("Delete"))
// Should contain ANSI color codes (resolved from palette.error)
#expect(allContent.contains("\u{1b}["))
}
@Test("Primary button is bold")
func primaryButtonIsBold() {
let context = createTestContext()
let button = Button("Submit", style: .primary) {}
let buffer = renderToBuffer(button, context: context)
let allContent = buffer.lines.joined()
// Primary style sets isBold = true, rendered as bold ANSI
#expect(allContent.contains("\u{1b}[1;"))
}
}
// MARK: - Action Handler Tests
@MainActor
@Suite("Action Handler Tests")
struct ActionHandlerTests {
@Test("ActionHandler handles Enter key")
func handleEnterKey() {
var wasTriggered = false
let handler = ActionHandler(
focusID: "enter-test",
action: { wasTriggered = true },
canBeFocused: true
)
let event = KeyEvent(key: .enter)
let handled = handler.handleKeyEvent(event)
#expect(handled == true)
#expect(wasTriggered == true)
}
@Test("ActionHandler handles Space key")
func handleSpaceKey() {
var wasTriggered = false
let handler = ActionHandler(
focusID: "space-test",
action: { wasTriggered = true },
canBeFocused: true
)
let event = KeyEvent(key: .character(" "))
let handled = handler.handleKeyEvent(event)
#expect(handled == true)
#expect(wasTriggered == true)
}
@Test("ActionHandler ignores other keys")
func ignoresOtherKeys() {
var wasTriggered = false
let handler = ActionHandler(
focusID: "ignore-test",
action: { wasTriggered = true },
canBeFocused: true
)
let event = KeyEvent(key: .character("a"))
let handled = handler.handleKeyEvent(event)
#expect(handled == false)
#expect(wasTriggered == false)
}
@Test("ActionHandler respects custom trigger keys")
func customTriggerKeys() {
var wasTriggered = false
let handler = ActionHandler(
focusID: "custom-test",
action: { wasTriggered = true },
canBeFocused: true,
triggerKeys: [Key.enter] // Only Enter, not Space
)
// Space should not trigger
let spaceEvent = KeyEvent(key: .character(" "))
let spaceHandled = handler.handleKeyEvent(spaceEvent)
#expect(spaceHandled == false)
#expect(wasTriggered == false)
// Enter should trigger
let enterEvent = KeyEvent(key: .enter)
let enterHandled = handler.handleKeyEvent(enterEvent)
#expect(enterHandled == true)
#expect(wasTriggered == true)
}
}
// MARK: - Button Row Tests
@MainActor
@Suite("Button Row Tests")
struct ButtonRowTests {
@Test("ButtonRow can be created with buttons")
func buttonRowCreation() {
let context = createTestContext()
let row = ButtonRow {
Button("Cancel") {}
Button("OK") {}
}
let buffer = renderToBuffer(row, context: context)
// Bracket-style buttons are single line
#expect(buffer.height == 1)
let allContent = buffer.lines.joined()
#expect(allContent.contains("Cancel"))
#expect(allContent.contains("OK"))
}
@Test("ButtonRow with custom spacing")
func buttonRowSpacing() {
let context = createTestContext()
let row = ButtonRow(spacing: 5) {
Button("A", style: .plain) {}
Button("B", style: .plain) {}
}
let buffer = renderToBuffer(row, context: context)
// Both buttons should be present
#expect(buffer.height == 1) // plain buttons without border
let allContent = buffer.lines.joined()
#expect(allContent.contains("A"))
#expect(allContent.contains("B"))
}
@Test("Empty ButtonRow returns empty buffer")
func emptyButtonRow() {
let row = ButtonRow {}
let context = createTestContext()
let buffer = renderToBuffer(row, context: context)
#expect(buffer.isEmpty)
}
@Test("ButtonRow renders buttons horizontally")
func buttonRowHorizontal() {
let context = createTestContext()
let row = ButtonRow {
Button("First", style: .plain) {}
Button("Second", style: .plain) {}
}
let buffer = renderToBuffer(row, context: context)
// Should have same number of lines (horizontal layout)
// Plain buttons are single line, so the row should be single line
#expect(buffer.height == 1)
}
@Test("ButtonRow with mixed styles has uniform height")
func buttonRowUniformHeight() {
let context = createTestContext()
let row = ButtonRow {
Button("Default") {}
Button("Plain", style: .plain) {}
}
let buffer = renderToBuffer(row, context: context)
// Both are now single line (brackets and plain)
#expect(buffer.height == 1)
}
}
// MARK: - Button Row Builder Tests
@MainActor
@Suite("Button Row Builder Tests")
struct ButtonRowBuilderTests {
@Test("ButtonRowBuilder builds array of buttons")
func builderCreatesArray() {
let buttons = ButtonRowBuilder.buildBlock(
Button("A") {},
Button("B") {},
Button("C") {}
)
#expect(buttons.count == 3)
}
@Test("ButtonRowBuilder handles optional")
func builderHandlesOptional() {
let buttons: [Button]? = nil
let result = ButtonRowBuilder.buildOptional(buttons)
#expect(result.isEmpty)
let someButtons: [Button]? = [Button("Test") {}]
let result2 = ButtonRowBuilder.buildOptional(someButtons)
#expect(result2.count == 1)
}
@Test("ButtonRowBuilder handles either first")
func builderHandlesEitherFirst() {
let buttons = [Button("First") {}]
let result = ButtonRowBuilder.buildEither(first: buttons)
#expect(result.count == 1)
#expect(result[0].label == "First")
}
@Test("ButtonRowBuilder handles either second")
func builderHandlesEitherSecond() {
let buttons = [Button("Second") {}]
let result = ButtonRowBuilder.buildEither(second: buttons)
#expect(result.count == 1)
#expect(result[0].label == "Second")
}
@Test("ButtonRowBuilder handles array")
func builderHandlesArray() {
let groups: [[Button]] = [
[Button("A") {}],
[Button("B") {}, Button("C") {}],
]
let result = ButtonRowBuilder.buildArray(groups)
#expect(result.count == 3)
}
}