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

318 lines
10 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
// ProgressViewTests.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 = 30, height: Int = 24) -> RenderContext {
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
}
// MARK: - ProgressView Rendering Tests
@MainActor
@Suite("ProgressView Tests")
struct ProgressViewTests {
@Test("Progress bar renders single line without label")
func barOnlyIsSingleLine() {
let view = ProgressView(value: 0.5)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
#expect(buffer.height == 1)
#expect(buffer.width == 30)
}
@Test("Progress bar with label renders two lines")
func barWithLabelIsTwoLines() {
let view = ProgressView("Loading", value: 0.5)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
#expect(buffer.height == 2)
#expect(buffer.lines[0].contains("Loading"))
}
@Test("Progress bar with ViewBuilder label renders two lines")
func barWithViewBuilderLabel() {
let view = ProgressView(value: 0.7) {
Text("Downloading")
}
let context = testContext()
let buffer = renderToBuffer(view, context: context)
#expect(buffer.height == 2)
#expect(buffer.lines[0].contains("Downloading"))
}
@Test("Progress bar with label and currentValueLabel shows both")
func barWithLabelAndValueLabel() {
let view = ProgressView(value: 0.5) {
Text("Task")
} currentValueLabel: {
Text("50%")
}
let context = testContext()
let buffer = renderToBuffer(view, context: context)
#expect(buffer.height == 2)
#expect(buffer.lines[0].contains("Task"))
#expect(buffer.lines[0].contains("50%"))
}
@Test("Default line style contains filled and empty block characters")
func lineStyleContainsBlockCharacters() {
let view = ProgressView(value: 0.5)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.contains(""))
#expect(barLine.contains(""))
}
@Test("0% progress shows all empty blocks")
func zeroProgressAllEmpty() {
let view = ProgressView(value: 0.0)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(!barLine.contains(""))
#expect(barLine.contains(""))
}
@Test("100% progress shows all filled blocks")
func fullProgressAllFilled() {
let view = ProgressView(value: 1.0)
let context = testContext()
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.contains(""))
#expect(!barLine.contains(""))
}
@Test("Bar width equals available width")
func barFillsAvailableWidth() {
let view = ProgressView(value: 0.5)
let context = testContext(width: 20)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.count == 20)
}
@Test("Filled count scales with fraction at 50%")
func filledCountScalesWithFraction() {
let view = ProgressView(value: 0.5)
let context = testContext(width: 20)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let filledCount = barLine.filter { $0 == "" }.count
let emptyCount = barLine.filter { $0 == "" }.count
#expect(filledCount == 10)
#expect(emptyCount == 10)
}
}
// MARK: - Style Tests
@MainActor
@Suite("ProgressView Style Tests")
struct ProgressViewStyleTests {
@Test("Block style uses only █ and ░ characters")
func blockStyleWholeBlocks() {
let view = ProgressView(value: 0.33).trackStyle(.block)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let allExpected = barLine.allSatisfy { $0 == "" || $0 == "" }
#expect(allExpected)
}
@Test("BlockFine style uses fractional blocks for sub-character precision")
func blockFineStyleFractionalBlocks() {
// 33% of 10 = 3.3 cells 3 full + fractional
let view = ProgressView(value: 0.33).trackStyle(.blockFine)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let fractionalChars: Set<Character> = ["", "", "", "", "", "", ""]
let hasFractional = barLine.contains { fractionalChars.contains($0) }
#expect(hasFractional)
}
@Test("Shade style uses ▓ and ░ characters")
func shadeStyleCharacters() {
let view = ProgressView(value: 0.5).trackStyle(.shade)
let context = testContext(width: 20)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.contains(""))
#expect(barLine.contains(""))
}
@Test("Bar style uses ▌ and ─ characters")
func barStyleCharacters() {
let view = ProgressView(value: 0.5).trackStyle(.bar)
let context = testContext(width: 20)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.contains(""))
#expect(barLine.contains(""))
}
@Test("Dot style uses ▬, ● head, and ─ characters")
func dotStyleCharacters() {
let view = ProgressView(value: 0.5).trackStyle(.dot)
let context = testContext(width: 20)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.contains(""))
#expect(barLine.contains(""))
#expect(barLine.contains(""))
}
@Test("Style modifier returns correct style")
func styleModifierWorks() {
let view = ProgressView(value: 0.5).trackStyle(.shade)
#expect(view.style == .shade)
}
@Test("All styles render correct width")
func allStylesCorrectWidth() {
let styles: [ProgressBarStyle] = [.block, .blockFine, .shade, .bar, .dot]
let context = testContext(width: 20)
for style in styles {
let view = ProgressView(value: 0.5).trackStyle(style)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.count == 20, "Style \(style) should render width 20, got \(barLine.count)")
}
}
@Test("Dot style at 0% shows no head and all empty")
func dotStyleZeroPercent() {
let view = ProgressView(value: 0.0).trackStyle(.dot)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(!barLine.contains(""))
#expect(!barLine.contains(""))
#expect(barLine.contains(""))
}
@Test("Dot style at 100% shows head at end")
func dotStyleFullPercent() {
let view = ProgressView(value: 1.0).trackStyle(.dot)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine.contains(""))
#expect(barLine.contains(""))
#expect(!barLine.contains(""))
}
}
// MARK: - Edge Case Tests
@MainActor
@Suite("ProgressView Edge Cases")
struct ProgressViewEdgeCaseTests {
@Test("Value greater than total clamps to 100%")
func valueExceedsTotalClamped() {
let view = ProgressView(value: 2.0, total: 1.0)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let filledCount = barLine.filter { $0 == "" }.count
#expect(filledCount == 10)
}
@Test("Negative value clamps to 0%")
func negativeValueClamped() {
let view = ProgressView(value: -0.5)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let filledCount = barLine.filter { $0 == "" }.count
#expect(filledCount == 0)
}
@Test("Zero total produces 0% bar")
func zeroTotalShowsEmpty() {
let view = ProgressView(value: 5.0, total: 0.0)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let filledCount = barLine.filter { $0 == "" }.count
#expect(filledCount == 0)
}
@Test("nil value renders empty bar (indeterminate fallback)")
func nilValueRendersEmptyBar() {
let view = ProgressView<EmptyView, EmptyView>(value: Optional<Double>.none)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let filledCount = barLine.filter { $0 == "" }.count
#expect(filledCount == 0)
}
@Test("Custom total works correctly")
func customTotal() {
let view = ProgressView(value: 3.0, total: 10.0)
let context = testContext(width: 10)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
let filledCount = barLine.filter { $0 == "" }.count
#expect(filledCount == 3) // 30% of 10
}
@Test("Float value works via BinaryFloatingPoint generic")
func floatValueWorks() {
let view = ProgressView(value: Float(0.5))
let context = testContext()
let buffer = renderToBuffer(view, context: context)
#expect(buffer.height == 1)
#expect(buffer.lines[0].contains(""))
}
@Test("Width of 1 renders single character")
func singleCharWidth() {
let view = ProgressView(value: 1.0)
let context = testContext(width: 1)
let buffer = renderToBuffer(view, context: context)
let barLine = buffer.lines[0].stripped
#expect(barLine == "")
}
}