Files
TUIkit/Tests/TUIkitTests/ModifierPropagationTests.swift
T
phranck 3fb4944472 Refactor: Move runtime services from RenderContext to EnvironmentValues
- Add ServiceEnvironment.swift with 9 EnvironmentKeys for runtime services
  (stateStorage, lifecycle, keyEventDispatcher, renderCache, preferenceStorage,
  pulsePhase, cursorTimer, focusIndicatorColor, activeFocusSectionID)
- Remove tuiContext, pulsePhase, cursorTimer, focusIndicatorColor, and
  activeFocusSectionID as direct RenderContext properties
- Inject all services through EnvironmentValues in RenderLoop.buildEnvironment()
- Add convenience RenderContext init that accepts TUIContext and auto-injects
  services into the environment
- Simplify isolatedForBackground() to only swap environment values
- Migrate ~49 access sites in ~25 source files from context.tuiContext.X and
  context.pulsePhase/cursorTimer to context.environment.X
- Update 38 test files to use the new convenience init
2026-02-14 13:13:24 +01:00

249 lines
8.0 KiB
Swift

// TUIKit - Terminal UI Kit for Swift
// ModifierPropagationTests.swift
//
// Created by LAYERED.work
// License: MIT
import Testing
@testable import TUIkit
// MARK: - Test Helpers
/// Creates a default render context for testing.
@MainActor
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
}
// MARK: - Modifier Propagation Tests
@MainActor
@Suite("Modifier Propagation Tests")
struct ModifierPropagationTests {
// MARK: - Environment Propagation
@Test("foregroundStyle propagates through View hierarchy")
func foregroundStylePropagates() {
// Create a view hierarchy with foregroundStyle applied at top level
let view = VStack {
Text("Hello")
Text("World")
}
.foregroundStyle(.red)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
// Both lines should contain red ANSI code
let redCode = "\u{1B}[31m"
#expect(buffer.lines[0].contains(redCode), "First text should have red color")
#expect(buffer.lines[1].contains(redCode), "Second text should have red color")
}
@Test("foregroundStyle propagates to nested containers")
func foregroundStylePropagatesNested() {
let view = VStack {
HStack {
Text("A")
Text("B")
}
}
.foregroundStyle(.green)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
// Content should have green color
let greenCode = "\u{1B}[32m"
#expect(buffer.lines[0].contains(greenCode), "Nested content should have green color")
}
@Test("Child foregroundStyle overrides parent")
func childForegroundStyleOverrides() {
let view = VStack {
Text("Red")
Text("Blue").foregroundStyle(.blue)
}
.foregroundStyle(.red)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
let redCode = "\u{1B}[31m"
let blueCode = "\u{1B}[34m"
#expect(buffer.lines[0].contains(redCode), "First text should be red")
#expect(buffer.lines[1].contains(blueCode), "Second text should be blue (overridden)")
}
// MARK: - Button Modifier Propagation
@Test("Button renders with body: some View pattern")
func buttonRendersCorrectly() {
var actionCalled = false
let button = Button("Test") {
actionCalled = true
}
let context = testContext()
let buffer = renderToBuffer(button, context: context)
// Button should render with caps
#expect(buffer.lines[0].stripped.contains("\u{2590}"))
#expect(buffer.lines[0].stripped.contains("Test"))
#expect(buffer.lines[0].stripped.contains("\u{258C}"))
#expect(!actionCalled, "Action should not be called during render")
}
@Test("Button disabled state propagates")
func buttonDisabledStatePropagates() {
let button = Button("Disabled") {}
.disabled(true)
let context = testContext()
let buffer = renderToBuffer(button, context: context)
// Disabled button should render (with dimmed styling)
#expect(buffer.lines[0].stripped.contains("Disabled"))
}
// MARK: - Toggle Modifier Propagation
@Test("Toggle renders with body: some View pattern")
func toggleRendersCorrectly() {
var isOn = false
let toggle = Toggle("Enable", isOn: Binding(get: { isOn }, set: { isOn = $0 }))
let context = testContext()
let buffer = renderToBuffer(toggle, context: context)
// Toggle should render with brackets and label
#expect(buffer.lines[0].stripped.contains("[") && buffer.lines[0].stripped.contains("]"))
#expect(buffer.lines[0].contains("Enable"))
}
@Test("Toggle disabled state propagates")
func toggleDisabledStatePropagates() {
var isOn = true
let toggle = Toggle("Disabled Toggle", isOn: Binding(get: { isOn }, set: { isOn = $0 }))
.disabled(true)
let context = testContext()
let buffer = renderToBuffer(toggle, context: context)
#expect(buffer.lines[0].contains("Disabled Toggle"))
}
// MARK: - Menu Modifier Propagation
@Test("Menu renders with body: some View pattern")
func menuRendersCorrectly() {
let menu = Menu(
title: "Test Menu",
items: [
MenuItem(label: "Item 1"),
MenuItem(label: "Item 2")
]
)
let context = testContext()
let buffer = renderToBuffer(menu, context: context)
// Menu should render with border and items
#expect(buffer.height > 2, "Menu should have multiple lines")
#expect(buffer.lines.joined().contains("Test Menu"))
#expect(buffer.lines.joined().contains("Item 1"))
#expect(buffer.lines.joined().contains("Item 2"))
}
// MARK: - RadioButtonGroup Modifier Propagation
@Test("RadioButtonGroup renders with body: some View pattern")
func radioButtonGroupRendersCorrectly() {
var selection = "a"
let group = RadioButtonGroup(
selection: Binding(get: { selection }, set: { selection = $0 })
) {
RadioButtonItem("a", "Option A")
RadioButtonItem("b", "Option B")
}
let context = testContext()
let buffer = renderToBuffer(group, context: context)
// RadioButtonGroup should render items vertically
#expect(buffer.height == 2, "Should have 2 lines for 2 items")
#expect(buffer.lines[0].contains("Option A"))
#expect(buffer.lines[1].contains("Option B"))
}
@Test("RadioButtonGroup disabled state propagates")
func radioButtonGroupDisabledStatePropagates() {
var selection = "a"
let group = RadioButtonGroup(
selection: Binding(get: { selection }, set: { selection = $0 })
) {
RadioButtonItem("a", "Option A")
}
.disabled(true)
let context = testContext()
let buffer = renderToBuffer(group, context: context)
#expect(buffer.lines[0].contains("Option A"))
}
// MARK: - Complex Hierarchy Tests
@Test("Modifiers propagate through complex hierarchy")
func modifiersPropagateComplexHierarchy() {
let view = VStack {
Text("Title").bold()
HStack {
Text("Left")
Text("Right")
}
Text("Footer")
}
.foregroundStyle(.cyan)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
// All text should have cyan color (36m, possibly with bold prefix 1;)
for line in buffer.lines where !line.stripped.isEmpty {
#expect(line.contains("36m"), "All non-empty lines should have cyan color code: \(line.stripped)")
}
}
@Test("Padding modifier works with refactored views")
func paddingWorksWithRefactoredViews() {
var actionCalled = false
let button = Button("Padded") { actionCalled = true }
.padding(2)
let context = testContext()
let buffer = renderToBuffer(button, context: context)
// Should have padding lines
#expect(buffer.height >= 3, "Should have top padding + content + bottom padding")
#expect(!actionCalled)
}
@Test("Border modifier works with refactored views")
func borderWorksWithRefactoredViews() {
var isOn = false
let toggle = Toggle("Bordered", isOn: Binding(get: { isOn }, set: { isOn = $0 }))
.border(.line)
let context = testContext()
let buffer = renderToBuffer(toggle, context: context)
// Should have border
#expect(buffer.height == 3, "Should have top border + content + bottom border")
#expect(buffer.lines[0].stripped.contains("\u{250C}") || buffer.lines[0].stripped.contains("\u{2500}"))
}
}