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

206 lines
6.2 KiB
Swift

// TUIKit - Terminal UI Kit for Swift
// FrameModifierTests.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: - FrameModifier Tests
@MainActor
@Suite("FrameModifier Tests")
struct FrameModifierTests {
@Test("FlexibleFrameView with maxWidth infinity fills available width")
func frameMaxWidthInfinity() {
let frame = FlexibleFrameView(
content: Text("Hi"),
minWidth: nil,
idealWidth: nil,
maxWidth: .infinity,
minHeight: nil,
idealHeight: nil,
maxHeight: nil,
alignment: .center
)
let context = testContext(width: 30)
let buffer = frame.renderToBuffer(context: context)
#expect(buffer.width == 30)
}
@Test("FlexibleFrameView with fixed maxWidth constrains")
func frameFixedMaxWidth() {
let frame = FlexibleFrameView(
content: Text("Short"),
minWidth: nil,
idealWidth: nil,
maxWidth: .fixed(10),
minHeight: nil,
idealHeight: nil,
maxHeight: nil,
alignment: .leading
)
let context = testContext(width: 40)
let buffer = frame.renderToBuffer(context: context)
// Content "Short" is 5 chars, no maxWidth expansion without infinity
#expect(buffer.width <= 10)
}
@Test("FlexibleFrameView with minWidth enforces minimum")
func frameMinWidth() {
let frame = FlexibleFrameView(
content: Text("Hi"),
minWidth: 10,
idealWidth: nil,
maxWidth: nil,
minHeight: nil,
idealHeight: nil,
maxHeight: nil,
alignment: .leading
)
let context = testContext(width: 40)
let buffer = frame.renderToBuffer(context: context)
#expect(buffer.width >= 10)
}
@Test("FlexibleFrameView with minHeight enforces minimum")
func frameMinHeight() {
let frame = FlexibleFrameView(
content: Text("Hi"),
minWidth: nil,
idealWidth: nil,
maxWidth: nil,
minHeight: 5,
idealHeight: nil,
maxHeight: nil,
alignment: .top
)
let context = testContext()
let buffer = frame.renderToBuffer(context: context)
#expect(buffer.height >= 5)
}
@Test("FlexibleFrameView alignment center")
func frameCenterAlignment() {
let frame = FlexibleFrameView(
content: Text("Hi"),
minWidth: 10,
idealWidth: nil,
maxWidth: nil,
minHeight: 3,
idealHeight: nil,
maxHeight: nil,
alignment: .center
)
let context = testContext()
let buffer = frame.renderToBuffer(context: context)
#expect(buffer.width >= 10)
#expect(buffer.height >= 3)
// Center vertically: content should be on line 1 (middle of 3)
let contentLine = buffer.lines[1]
#expect(contentLine.contains("Hi"))
// Center horizontally: "Hi" is 2 chars, frame is 10, so 4 spaces on left
let stripped = contentLine.stripped
let leadingSpaces = stripped.prefix(while: { $0 == " " }).count
#expect(leadingSpaces == 4, "Content should be horizontally centered with 4 leading spaces")
}
@Test("FlexibleFrameView alignment trailing")
func frameTrailingAlignment() {
let frame = FlexibleFrameView(
content: Text("Hi"),
minWidth: 10,
idealWidth: nil,
maxWidth: nil,
minHeight: nil,
idealHeight: nil,
maxHeight: nil,
alignment: .trailing
)
let context = testContext()
let buffer = frame.renderToBuffer(context: context)
// "Hi" should be right-aligned within 10 chars
let line = buffer.lines[0]
#expect(line.stripped.hasSuffix("Hi"))
}
@Test("FlexibleFrameView alignment bottom")
func frameBottomAlignment() {
let frame = FlexibleFrameView(
content: Text("Hi"),
minWidth: nil,
idealWidth: nil,
maxWidth: nil,
minHeight: 3,
idealHeight: nil,
maxHeight: nil,
alignment: .bottom
)
let context = testContext()
let buffer = frame.renderToBuffer(context: context)
#expect(buffer.height >= 3)
// Content on last line
let lastLine = buffer.lines[buffer.height - 1]
#expect(lastLine.contains("Hi"))
}
@Test("FlexibleFrameView maxHeight constrains available height for content")
func frameMaxHeight() {
// maxHeight constrains the availableHeight passed to child rendering,
// but does not clip content that exceeds constraints. This matches
// SwiftUI behavior where frame constraints inform layout, not clip.
let frame = FlexibleFrameView(
content: Text("Short"),
minWidth: nil,
idealWidth: nil,
maxWidth: nil,
minHeight: 5,
idealHeight: nil,
maxHeight: .fixed(10),
alignment: .top
)
let context = testContext()
let buffer = frame.renderToBuffer(context: context)
// minHeight 5 expands the 1-line content to 5 lines
#expect(buffer.height == 5)
}
@Test("FlexibleFrameView maxHeight infinity fills available space")
func frameMaxHeightInfinity() {
let frame = FlexibleFrameView(
content: Text("Hi"),
minWidth: nil,
idealWidth: nil,
maxWidth: nil,
minHeight: nil,
idealHeight: nil,
maxHeight: .infinity,
alignment: .top
)
var context = testContext()
context.availableHeight = 10
let buffer = frame.renderToBuffer(context: context)
// Should expand to fill available height
#expect(buffer.height == 10)
}
}