Files
TUIkit/Tests/TUIkitTests/ComponentViewTests.swift
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

223 lines
6.4 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 🖥 TUIKit Terminal UI Kit for Swift
// ComponentViewTests.swift
//
// Created by LAYERED.work
// License: MIT
import Testing
@testable import TUIkit
// MARK: - Test Helpers
/// Creates a default render context for testing.
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
}
// MARK: - Border via ContainerView Tests
@MainActor
@Suite("Border via ContainerView Tests")
struct BorderViaContainerViewTests {
@Test(".border() renders with border around content")
func borderRendersWithBorder() {
let view = Text("Hi").border(.line)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
// border = top + content + bottom = 3 lines minimum
#expect(buffer.height >= 3)
// Width = content width + 2 (left + right border) + 2 (padding)
#expect(buffer.width >= 6) // "Hi" (2) + borders (2) + padding (2)
}
@Test(".border() with empty content renders empty")
func borderEmptyContent() {
let view = EmptyView().border(.line)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
// EmptyView produces empty buffer, so bordered empty = empty
#expect(buffer.isEmpty)
}
@Test(".border() with VStack renders multiple lines")
func borderMultipleChildren() {
let view = VStack {
Text("Line 1")
Text("Line 2")
}.border(.line)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
// Top border + 2 content lines + bottom border = 4
#expect(buffer.height >= 4)
}
}
// MARK: - Card Tests
@MainActor
@Suite("Card Tests")
struct CardTests {
@Test("Card renders with border")
func cardRenders() {
let card = Card(title: "Test") {
Text("Hello")
}
let context = testContext()
let buffer = renderToBuffer(card, context: context)
// Should have top border + content + bottom border
#expect(buffer.height >= 3)
let allContent = buffer.lines.joined()
#expect(allContent.contains("Hello"))
#expect(allContent.contains("Test")) // title
}
@Test("Card without title renders")
func cardNoTitleRenders() {
let card = Card {
Text("Content")
}
let context = testContext()
let buffer = renderToBuffer(card, context: context)
#expect(buffer.height >= 3)
let allContent = buffer.lines.joined()
#expect(allContent.contains("Content"))
}
@Test("Card with footer is taller than without")
func cardFooterAddsHeight() {
let cardWithout = Card(title: "Test") {
Text("Body")
}
let cardWith = Card(title: "Test") {
Text("Body")
} footer: {
Text("Footer")
}
let context = testContext()
let bufferWithout = renderToBuffer(cardWithout, context: context)
let bufferWith = renderToBuffer(cardWith, context: context)
#expect(bufferWith.height > bufferWithout.height)
}
}
// MARK: - Panel Tests
@MainActor
@Suite("Panel Tests")
struct PanelTests {
@Test("Panel renders with border and title")
func panelRenders() {
let panel = Panel("Test Panel") {
Text("Hello")
}
let context = testContext()
let buffer = renderToBuffer(panel, context: context)
// Top border (with title) + content + bottom border
#expect(buffer.height >= 3)
let allContent = buffer.lines.joined()
#expect(allContent.contains("Test Panel")) // title
#expect(allContent.contains("Hello"))
}
@Test("Panel with footer is taller")
func panelFooterAddsHeight() {
let panelWithout = Panel("Test") {
Text("Body")
}
let panelWith = Panel("Test") {
Text("Body")
} footer: {
Text("Footer")
}
let context = testContext()
let bufferWithout = renderToBuffer(panelWithout, context: context)
let bufferWith = renderToBuffer(panelWith, context: context)
#expect(bufferWith.height > bufferWithout.height)
}
}
// MARK: - ContainerView Tests
@MainActor
@Suite("ContainerView Direct Tests")
struct ContainerViewDirectTests {
@Test("ContainerView renders with border")
func containerViewRenders() {
let container = ContainerView(title: "Test") {
Text("Content")
}
let context = testContext()
let buffer = renderToBuffer(container, context: context)
#expect(buffer.height >= 3)
let allContent = buffer.lines.joined()
#expect(allContent.contains("Test")) // title
#expect(allContent.contains("Content"))
}
}
// MARK: - ForEach Tests
@MainActor
@Suite("ForEach Tests")
struct ForEachTests {
struct TestItem: Identifiable {
let id: String
let name: String
}
@Test("ForEach generates correct number of views")
func forEachViewGeneration() {
let items = [TestItem(id: "a", name: "Alpha"), TestItem(id: "b", name: "Beta")]
let forEach = ForEach(items) { item in
Text(item.name)
}
// Verify content closure works
var generatedTexts: [String] = []
for item in forEach.data {
let view = forEach.content(item)
generatedTexts.append(view.content)
}
#expect(generatedTexts == ["Alpha", "Beta"])
}
// NOTE: ForEach inside VStack/HStack cannot be tested via renderToBuffer
// directly. ForEach is flattened into ViewArray by @ViewBuilder.buildArray
// at compile time not at render time. Direct construction in tests
// bypasses the builder, so ForEach remains unflattened and produces
// an empty buffer. This is expected behavior, matching SwiftUI's pattern.
@Test("ForEach with empty array produces empty result")
func forEachEmptyArray() {
let items: [TestItem] = []
let forEach = ForEach(items) { item in
Text(item.name)
}
#expect(forEach.data.isEmpty)
// Also test via ViewArray (which is what @ViewBuilder produces)
let viewArray = ViewArray<Text>([])
let context = testContext()
let buffer = renderToBuffer(viewArray, context: context)
#expect(buffer.isEmpty)
}
}