mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
cd2968e345
- Redesign Button rendering with half-block caps (U+2590/U+258C) and accent-tinted background - Apply matching cap style and background to TextField and SecureField - Add foregroundQuaternary to Palette for subtle UI elements like spinner tracks - Fix border saturation in all palette presets to preserve hue instead of going gray - Rework Spinner bouncing trail to interpolate from highlight to quaternary via Color.lerp - Fix Spinner label to use palette foreground color - Fix FrameDiffWriter to use ESC[2K for reliable line clearing - Add Layoutable conformance to NavigationSplitView for height-flexible layout - Update tests for new button cap style
247 lines
7.1 KiB
Swift
247 lines
7.1 KiB
Swift
// 🖥️ TUIKit — Terminal UI Kit for Swift
|
||
// FrameDiffWriterTests.swift
|
||
//
|
||
// Created by LAYERED.work
|
||
// License: MIT
|
||
|
||
import Testing
|
||
|
||
@testable import TUIkit
|
||
|
||
// MARK: - buildOutputLines Tests
|
||
|
||
@Suite("FrameDiffWriter buildOutputLines Tests")
|
||
@MainActor
|
||
struct BuildOutputLinesTests {
|
||
|
||
@Test("Produces exactly terminalHeight output lines")
|
||
func outputLineCount() {
|
||
let writer = FrameDiffWriter()
|
||
var buffer = FrameBuffer()
|
||
buffer.appendVertically(FrameBuffer(text: "Hello"))
|
||
buffer.appendVertically(FrameBuffer(text: "World"))
|
||
|
||
let lines = writer.buildOutputLines(
|
||
buffer: buffer,
|
||
terminalWidth: 20,
|
||
terminalHeight: 5,
|
||
bgCode: "",
|
||
reset: ""
|
||
)
|
||
|
||
#expect(lines.count == 5)
|
||
}
|
||
|
||
@Test("Content lines include background and padding")
|
||
func contentLinesHaveBgAndPadding() {
|
||
let writer = FrameDiffWriter()
|
||
let buffer = FrameBuffer(text: "Hi")
|
||
|
||
let lines = writer.buildOutputLines(
|
||
buffer: buffer,
|
||
terminalWidth: 10,
|
||
terminalHeight: 1,
|
||
bgCode: "[BG]",
|
||
reset: "[R]"
|
||
)
|
||
|
||
let line = lines[0]
|
||
#expect(line.hasPrefix("[BG]"))
|
||
#expect(line.hasSuffix("[R]"))
|
||
#expect(line.contains("Hi"))
|
||
}
|
||
|
||
@Test("Empty rows are filled with background-colored spaces")
|
||
func emptyRowsFilled() {
|
||
let writer = FrameDiffWriter()
|
||
let buffer = FrameBuffer()
|
||
|
||
let lines = writer.buildOutputLines(
|
||
buffer: buffer,
|
||
terminalWidth: 5,
|
||
terminalHeight: 2,
|
||
bgCode: "[BG]",
|
||
reset: "[R]"
|
||
)
|
||
|
||
// Empty lines use bgCode + ESC[2K (erase entire line with bg color) + reset
|
||
let eraseLine = "\u{1B}[2K"
|
||
let expected = "[BG]" + eraseLine + "[R]"
|
||
#expect(lines[0] == expected)
|
||
#expect(lines[1] == expected)
|
||
}
|
||
|
||
@Test("ANSI reset codes in content are replaced with reset+bg")
|
||
func resetCodesReplaced() {
|
||
let writer = FrameDiffWriter()
|
||
let reset = ANSIRenderer.reset
|
||
let buffer = FrameBuffer(text: "A\(reset)B")
|
||
|
||
let lines = writer.buildOutputLines(
|
||
buffer: buffer,
|
||
terminalWidth: 20,
|
||
terminalHeight: 1,
|
||
bgCode: "[BG]",
|
||
reset: reset
|
||
)
|
||
|
||
#expect(lines[0].contains("\(reset)[BG]"))
|
||
}
|
||
|
||
@Test("Multiple content lines are all processed")
|
||
func multipleContentLines() {
|
||
let writer = FrameDiffWriter()
|
||
var buffer = FrameBuffer()
|
||
buffer.appendVertically(FrameBuffer(text: "Line1"))
|
||
buffer.appendVertically(FrameBuffer(text: "Line2"))
|
||
buffer.appendVertically(FrameBuffer(text: "Line3"))
|
||
|
||
let lines = writer.buildOutputLines(
|
||
buffer: buffer,
|
||
terminalWidth: 10,
|
||
terminalHeight: 3,
|
||
bgCode: "",
|
||
reset: ""
|
||
)
|
||
|
||
#expect(lines[0].contains("Line1"))
|
||
#expect(lines[1].contains("Line2"))
|
||
#expect(lines[2].contains("Line3"))
|
||
}
|
||
}
|
||
|
||
// MARK: - Line Diff Logic Tests
|
||
|
||
@Suite("FrameDiffWriter Diff Logic Tests")
|
||
@MainActor
|
||
struct DiffLogicTests {
|
||
|
||
@Test("computeChangedRows returns all rows when previous is empty")
|
||
func allRowsChangedOnFirstFrame() {
|
||
let changed = FrameDiffWriter.computeChangedRows(
|
||
newLines: ["A", "B", "C"],
|
||
previousLines: []
|
||
)
|
||
|
||
#expect(changed == [0, 1, 2])
|
||
}
|
||
|
||
@Test("computeChangedRows returns empty when frames are identical")
|
||
func noChangesForIdenticalFrames() {
|
||
let lines = ["A", "B", "C"]
|
||
let changed = FrameDiffWriter.computeChangedRows(
|
||
newLines: lines,
|
||
previousLines: lines
|
||
)
|
||
|
||
#expect(changed.isEmpty)
|
||
}
|
||
|
||
@Test("computeChangedRows detects single changed line")
|
||
func singleLineChanged() {
|
||
let changed = FrameDiffWriter.computeChangedRows(
|
||
newLines: ["A", "X", "C"],
|
||
previousLines: ["A", "B", "C"]
|
||
)
|
||
|
||
#expect(changed == [1])
|
||
}
|
||
|
||
@Test("computeChangedRows detects multiple changed lines")
|
||
func multipleLinesChanged() {
|
||
let changed = FrameDiffWriter.computeChangedRows(
|
||
newLines: ["A", "X", "C", "Y"],
|
||
previousLines: ["A", "B", "C", "D"]
|
||
)
|
||
|
||
#expect(changed == [1, 3])
|
||
}
|
||
|
||
@Test("computeChangedRows handles new lines longer than previous")
|
||
func newLinesLongerThanPrevious() {
|
||
let changed = FrameDiffWriter.computeChangedRows(
|
||
newLines: ["A", "B", "C", "D"],
|
||
previousLines: ["A", "B"]
|
||
)
|
||
|
||
// C and D are new (indices 2, 3)
|
||
#expect(changed == [2, 3])
|
||
}
|
||
|
||
@Test("computeChangedRows handles ANSI-coded strings correctly")
|
||
func ansiStringComparison() {
|
||
let styledA = "\u{1B}[31mRed\u{1B}[0m"
|
||
let styledB = "\u{1B}[32mGreen\u{1B}[0m"
|
||
|
||
let changed = FrameDiffWriter.computeChangedRows(
|
||
newLines: [styledA, styledB],
|
||
previousLines: [styledA, styledA]
|
||
)
|
||
|
||
// Only the second line changed (red → green)
|
||
#expect(changed == [1])
|
||
}
|
||
}
|
||
|
||
// MARK: - Integration Tests
|
||
|
||
@Suite("FrameDiffWriter Integration Tests")
|
||
@MainActor
|
||
struct DiffIntegrationTests {
|
||
|
||
@Test("Content and status bar caches are independent")
|
||
func independentCaches() {
|
||
let writer = FrameDiffWriter()
|
||
|
||
// Simulate writing content + status bar (using internal state check)
|
||
let contentLines = ["Content1", "Content2"]
|
||
let statusLines = ["Status1"]
|
||
|
||
// After writeContentDiff, content cache is set
|
||
// After writeStatusBarDiff, status cache is set
|
||
// We verify via computeChangedRows that each cache tracks independently
|
||
|
||
// First: content has all changed (empty previous)
|
||
let contentChanged1 = FrameDiffWriter.computeChangedRows(
|
||
newLines: contentLines,
|
||
previousLines: []
|
||
)
|
||
#expect(contentChanged1 == [0, 1])
|
||
|
||
// Status also has all changed (different previous)
|
||
let statusChanged1 = FrameDiffWriter.computeChangedRows(
|
||
newLines: statusLines,
|
||
previousLines: []
|
||
)
|
||
#expect(statusChanged1 == [0])
|
||
|
||
// Same content → no changes
|
||
let contentChanged2 = FrameDiffWriter.computeChangedRows(
|
||
newLines: contentLines,
|
||
previousLines: contentLines
|
||
)
|
||
#expect(contentChanged2.isEmpty)
|
||
|
||
// Status changed → only status
|
||
let statusChanged2 = FrameDiffWriter.computeChangedRows(
|
||
newLines: ["NEW Status"],
|
||
previousLines: statusLines
|
||
)
|
||
#expect(statusChanged2 == [0])
|
||
}
|
||
|
||
@Test("invalidate clears both content and status bar caches")
|
||
func invalidateClearsBothCaches() {
|
||
let writer = FrameDiffWriter()
|
||
|
||
// After invalidate, previous lines are empty → all rows changed
|
||
writer.invalidate()
|
||
|
||
let changed = FrameDiffWriter.computeChangedRows(
|
||
newLines: ["A", "B"],
|
||
previousLines: []
|
||
)
|
||
#expect(changed == [0, 1])
|
||
}
|
||
}
|