mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
3fb4944472
- 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
249 lines
8.0 KiB
Swift
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}"))
|
|
}
|
|
}
|