diff --git a/Tests/TUIkitTests/ComponentViewTests.swift b/Tests/TUIkitTests/ComponentViewTests.swift new file mode 100644 index 0000000..66fbda3 --- /dev/null +++ b/Tests/TUIkitTests/ComponentViewTests.swift @@ -0,0 +1,417 @@ +// +// ComponentViewTests.swift +// TUIkit +// +// Tests for Box, Card, Panel, ContainerConfig, ContainerView, and ForEach views. +// + +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) +} + +// MARK: - Box Tests + +@Suite("Box Tests") +struct BoxTests { + + @Test("Box can be created with default style") + func boxDefaultCreation() { + let box = Box { + Text("Hello") + } + #expect(box.borderStyle == nil) + #expect(box.borderColor == nil) + } + + @Test("Box can be created with explicit border style") + func boxExplicitStyle() { + let box = Box(.doubleLine, color: .cyan) { + Text("Styled") + } + #expect(box.borderStyle == .doubleLine) + #expect(box.borderColor == .cyan) + } + + @Test("Box renders with border around content") + func boxRendersWithBorder() { + let box = Box(.line) { + Text("Hi") + } + let context = testContext() + let buffer = renderToBuffer(box, context: context) + + // Box = border top + content + border bottom = 3 lines minimum + #expect(buffer.height >= 3) + // Width = content width + 2 (left + right border) + #expect(buffer.width >= 4) // "Hi" (2) + borders (2) + } + + @Test("Box renders empty content") + func boxEmptyContent() { + let box = Box(.line) { + EmptyView() + } + let context = testContext() + let buffer = renderToBuffer(box, context: context) + + // EmptyView produces empty buffer, so bordered empty = empty + #expect(buffer.isEmpty) + } + + @Test("Box with multiple children renders vertically") + func boxMultipleChildren() { + let box = Box(.line) { + Text("Line 1") + Text("Line 2") + } + let context = testContext() + let buffer = renderToBuffer(box, context: context) + + // Top border + 2 content lines + bottom border = 4 + #expect(buffer.height >= 4) + } + + @Test("Box delegates to body (is composite, not Renderable)") + func boxIsComposite() { + let box = Box { + Text("Test") + } + // Box should NOT conform to Renderable — it uses body + #expect(!(box is Renderable)) + } +} + +// MARK: - Card Tests + +@Suite("Card Tests") +struct CardTests { + + @Test("Card can be created without title") + func cardNoTitle() { + let card = Card { + Text("Content") + } + #expect(card.title == nil) + #expect(card.footer == nil) + #expect(card.backgroundColor == nil) + } + + @Test("Card can be created with title") + func cardWithTitle() { + let card = Card(title: "My Card") { + Text("Content") + } + #expect(card.title == "My Card") + } + + @Test("Card can be created with footer") + func cardWithFooter() { + let card = Card(title: "Info") { + Text("Body") + } footer: { + Text("Footer") + } + #expect(card.title == "Info") + #expect(card.footer != nil) + } + + @Test("Card can be created with custom border style") + func cardCustomStyle() { + let card = Card( + title: "Styled", + borderStyle: .doubleLine, + borderColor: .cyan, + titleColor: .brightYellow, + backgroundColor: .blue + ) { + Text("Content") + } + #expect(card.title == "Styled") + #expect(card.config.borderStyle == .doubleLine) + #expect(card.config.borderColor == .cyan) + #expect(card.config.titleColor == .brightYellow) + #expect(card.backgroundColor == .blue) + } + + @Test("Card renders with border") + func cardRenders() { + let card = Card(title: "Test") { + Text("Hello") + } + let context = testContext() + let buffer = card.renderToBuffer(context: context) + + // Should have top border + content + bottom border + #expect(buffer.height >= 3) + #expect(!buffer.isEmpty) + } + + @Test("Card without title renders") + func cardNoTitleRenders() { + let card = Card { + Text("Content") + } + let context = testContext() + let buffer = card.renderToBuffer(context: context) + + #expect(buffer.height >= 3) + #expect(!buffer.isEmpty) + } + + @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 = cardWithout.renderToBuffer(context: context) + let bufferWith = cardWith.renderToBuffer(context: context) + + #expect(bufferWith.height > bufferWithout.height) + } + + @Test("Card conforms to Renderable") + func cardIsRenderable() { + let card = Card { Text("Test") } + #expect(card is Renderable) + } +} + +// MARK: - Panel Tests + +@Suite("Panel Tests") +struct PanelTests { + + @Test("Panel can be created with title") + func panelCreation() { + let panel = Panel("Settings") { + Text("Option 1") + } + #expect(panel.title == "Settings") + #expect(panel.footer == nil) + } + + @Test("Panel can be created with footer") + func panelWithFooter() { + let panel = Panel("Info") { + Text("Content") + } footer: { + Text("Done") + } + #expect(panel.title == "Info") + #expect(panel.footer != nil) + } + + @Test("Panel can be created with custom style") + func panelCustomStyle() { + let panel = Panel( + "Styled", + borderStyle: .heavy, + borderColor: .red, + titleColor: .yellow + ) { + Text("Content") + } + #expect(panel.config.borderStyle == .heavy) + #expect(panel.config.borderColor == .red) + #expect(panel.config.titleColor == .yellow) + } + + @Test("Panel renders with border and title") + func panelRenders() { + let panel = Panel("Test Panel") { + Text("Hello") + } + let context = testContext() + let buffer = panel.renderToBuffer(context: context) + + // Top border (with title) + content + bottom border + #expect(buffer.height >= 3) + #expect(!buffer.isEmpty) + } + + @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 = panelWithout.renderToBuffer(context: context) + let bufferWith = panelWith.renderToBuffer(context: context) + + #expect(bufferWith.height > bufferWithout.height) + } + + @Test("Panel conforms to Renderable") + func panelIsRenderable() { + let panel = Panel("Title") { Text("Content") } + #expect(panel is Renderable) + } + + @Test("Panel default padding is horizontal only") + func panelDefaultPadding() { + let panel = Panel("Test") { + Text("Content") + } + #expect(panel.config.padding.leading == 1) + #expect(panel.config.padding.trailing == 1) + #expect(panel.config.padding.top == 0) + #expect(panel.config.padding.bottom == 0) + } +} + +// MARK: - ContainerConfig Tests + +@Suite("ContainerConfig Tests") +struct ContainerConfigTests { + + @Test("Default config has expected values") + func defaultConfig() { + let config = ContainerConfig.default + #expect(config.borderStyle == nil) + #expect(config.borderColor == nil) + #expect(config.titleColor == nil) + #expect(config.padding.leading == 1) + #expect(config.padding.trailing == 1) + #expect(config.showFooterSeparator == true) + } + + @Test("Custom config stores all values") + func customConfig() { + let config = ContainerConfig( + borderStyle: .doubleLine, + borderColor: .cyan, + titleColor: .yellow, + padding: EdgeInsets(all: 2), + showFooterSeparator: false + ) + #expect(config.borderStyle == .doubleLine) + #expect(config.borderColor == .cyan) + #expect(config.titleColor == .yellow) + #expect(config.padding.top == 2) + #expect(config.padding.leading == 2) + #expect(config.showFooterSeparator == false) + } +} + +// MARK: - ContainerView Tests + +@Suite("ContainerView Direct Tests") +struct ContainerViewDirectTests { + + @Test("ContainerView can be created without title") + func containerViewNoTitle() { + let container = ContainerView { + Text("Content") + } + #expect(container.title == nil) + #expect(container.footer == nil) + } + + @Test("ContainerView can be created with title and footer") + func containerViewWithTitleAndFooter() { + let container = ContainerView(title: "Header") { + Text("Body") + } footer: { + Text("Footer") + } + #expect(container.title == "Header") + #expect(container.footer != nil) + } + + @Test("ContainerView renders with border") + func containerViewRenders() { + let container = ContainerView(title: "Test") { + Text("Content") + } + let context = testContext() + let buffer = container.renderToBuffer(context: context) + + #expect(buffer.height >= 3) + #expect(!buffer.isEmpty) + } + + @Test("ContainerView conforms to Renderable") + func containerViewIsRenderable() { + let container = ContainerView { Text("Test") } + #expect(container is Renderable) + } +} + +// MARK: - ForEach Tests + +@Suite("ForEach Tests") +struct ForEachTests { + + struct TestItem: Identifiable { + let id: String + let name: String + } + + @Test("ForEach can be created with Identifiable data") + func forEachIdentifiable() { + let items = [TestItem(id: "1", name: "One"), TestItem(id: "2", name: "Two")] + let forEach = ForEach(items) { item in + Text(item.name) + } + #expect(forEach.data.count == 2) + } + + @Test("ForEach can be created with explicit ID key path") + func forEachExplicitId() { + let names = ["Anna", "Bob", "Clara"] + let forEach = ForEach(names, id: \.self) { name in + Text(name) + } + #expect(forEach.data.count == 3) + } + + @Test("ForEach can be created with Range") + func forEachRange() { + let forEach = ForEach(0..<5) { index in + Text("Item \(index)") + } + #expect(forEach.data.count == 5) + } + + @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. +} diff --git a/Tests/TUIkitTests/ModifierTests.swift b/Tests/TUIkitTests/ModifierTests.swift new file mode 100644 index 0000000..62ff0f8 --- /dev/null +++ b/Tests/TUIkitTests/ModifierTests.swift @@ -0,0 +1,620 @@ +// +// ModifierTests.swift +// TUIkit +// +// Tests for Frame, Padding, Border, and Background modifiers. +// + +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) +} + +// MARK: - EdgeInsets Tests + +@Suite("EdgeInsets Tests") +struct EdgeInsetsTests { + + @Test("EdgeInsets individual values") + func edgeInsetsIndividual() { + let insets = EdgeInsets(top: 1, leading: 2, bottom: 3, trailing: 4) + #expect(insets.top == 1) + #expect(insets.leading == 2) + #expect(insets.bottom == 3) + #expect(insets.trailing == 4) + } + + @Test("EdgeInsets uniform value") + func edgeInsetsUniform() { + let insets = EdgeInsets(all: 3) + #expect(insets.top == 3) + #expect(insets.leading == 3) + #expect(insets.bottom == 3) + #expect(insets.trailing == 3) + } + + @Test("EdgeInsets horizontal and vertical") + func edgeInsetsHorizontalVertical() { + let insets = EdgeInsets(horizontal: 2, vertical: 1) + #expect(insets.top == 1) + #expect(insets.leading == 2) + #expect(insets.bottom == 1) + #expect(insets.trailing == 2) + } + + @Test("EdgeInsets default values are zero") + func edgeInsetsDefaults() { + let insets = EdgeInsets() + #expect(insets.top == 0) + #expect(insets.leading == 0) + #expect(insets.bottom == 0) + #expect(insets.trailing == 0) + } + + @Test("EdgeInsets is Equatable") + func edgeInsetsEquatable() { + let insetsA = EdgeInsets(all: 2) + let insetsB = EdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2) + #expect(insetsA == insetsB) + } +} + +// MARK: - Edge Tests + +@Suite("Edge Tests") +struct EdgeTests { + + @Test("Individual edges exist") + func individualEdges() { + #expect(Edge.top.rawValue == 1) + #expect(Edge.leading.rawValue == 2) + #expect(Edge.bottom.rawValue == 4) + #expect(Edge.trailing.rawValue == 8) + } + + @Test("Edge.all contains all edges") + func edgeAll() { + #expect(Edge.all.contains(.top)) + #expect(Edge.all.contains(.leading)) + #expect(Edge.all.contains(.bottom)) + #expect(Edge.all.contains(.trailing)) + } + + @Test("Edge.horizontal contains leading and trailing") + func edgeHorizontal() { + #expect(Edge.horizontal.contains(.leading)) + #expect(Edge.horizontal.contains(.trailing)) + #expect(!Edge.horizontal.contains(.top)) + #expect(!Edge.horizontal.contains(.bottom)) + } + + @Test("Edge.vertical contains top and bottom") + func edgeVertical() { + #expect(Edge.vertical.contains(.top)) + #expect(Edge.vertical.contains(.bottom)) + #expect(!Edge.vertical.contains(.leading)) + #expect(!Edge.vertical.contains(.trailing)) + } +} + +// MARK: - PaddingModifier Tests + +@Suite("PaddingModifier Tests") +struct PaddingModifierTests { + + @Test("Padding adds empty lines for top and bottom") + func paddingTopBottom() { + let modifier = PaddingModifier(insets: EdgeInsets(top: 1, leading: 0, bottom: 1, trailing: 0)) + let buffer = FrameBuffer(lines: ["Hello"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + // 1 top + 1 content + 1 bottom = 3 + #expect(result.height == 3) + #expect(result.lines[0].trimmingCharacters(in: .whitespaces) == "") + #expect(result.lines[1] == "Hello") + #expect(result.lines[2].trimmingCharacters(in: .whitespaces) == "") + } + + @Test("Padding adds spaces for leading and trailing") + func paddingLeadingTrailing() { + let modifier = PaddingModifier(insets: EdgeInsets(top: 0, leading: 2, bottom: 0, trailing: 3)) + let buffer = FrameBuffer(lines: ["Hi"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + #expect(result.height == 1) + // " Hi " — 2 leading + "Hi" + 3 trailing + #expect(result.lines[0] == " Hi ") + } + + @Test("Padding all sides") + func paddingAllSides() { + let modifier = PaddingModifier(insets: EdgeInsets(all: 1)) + let buffer = FrameBuffer(lines: ["X"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + // 1 top + 1 content + 1 bottom = 3 + #expect(result.height == 3) + // Content line: 1 leading + "X" + 1 trailing = " X " + #expect(result.lines[1] == " X ") + } + + @Test("Padding on empty buffer returns empty") + func paddingEmptyBuffer() { + let modifier = PaddingModifier(insets: EdgeInsets(all: 2)) + let buffer = FrameBuffer() + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + // Only padding lines (no content) + #expect(result.height == 4) // 2 top + 0 content + 2 bottom + } + + @Test("Padding preserves multiple content lines") + func paddingMultipleLines() { + let modifier = PaddingModifier(insets: EdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1)) + let buffer = FrameBuffer(lines: ["AAA", "BBB"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + // 1 top + 2 content + 1 bottom = 4 + #expect(result.height == 4) + #expect(result.lines[1] == " AAA ") + #expect(result.lines[2] == " BBB ") + } + + @Test("Zero padding returns original dimensions") + func paddingZero() { + let modifier = PaddingModifier(insets: EdgeInsets()) + let buffer = FrameBuffer(lines: ["Test"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + #expect(result.height == 1) + #expect(result.lines[0] == "Test") + } +} + +// MARK: - FrameModifier Tests + +@Suite("FrameModifier Tests") +struct FrameModifierTests { + + @Test("FrameDimension.fixed stores value") + func frameDimensionFixed() { + let dim = FrameDimension.fixed(10) + #expect(dim == .fixed(10)) + } + + @Test("FrameDimension.infinity and .max are equal") + func frameDimensionInfinity() { + #expect(FrameDimension.infinity == .max) + } + + @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")) + } + + @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.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 conforms to Renderable") + func frameIsRenderable() { + let frame = FlexibleFrameView( + content: Text("Test"), + minWidth: nil, idealWidth: nil, maxWidth: nil, + minHeight: nil, idealHeight: nil, maxHeight: nil, + alignment: .center + ) + #expect(frame is Renderable) + } +} + +// MARK: - BorderModifier Tests + +@Suite("BorderModifier Tests") +struct BorderModifierTests { + + @Test("BorderedView can be created") + func borderedViewCreation() { + let bordered = BorderedView(content: Text("Hi"), style: .line, color: .green) + #expect(bordered.style == .line) + #expect(bordered.color == .green) + } + + @Test("BorderedView renders with top and bottom borders") + func borderedViewRenders() { + let bordered = BorderedView(content: Text("Test"), style: .line, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + // Top border + content + bottom border + #expect(buffer.height == 3) + #expect(!buffer.isEmpty) + } + + @Test("BorderedView with empty content returns empty") + func borderedViewEmptyContent() { + let bordered = BorderedView(content: EmptyView(), style: .line, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + #expect(buffer.isEmpty) + } + + @Test("BorderedView with line style uses correct corner characters") + func borderedViewLineStyle() { + let bordered = BorderedView(content: Text("X"), style: .line, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + let topLine = buffer.lines[0].stripped + let bottomLine = buffer.lines[buffer.height - 1].stripped + + #expect(topLine.hasPrefix("┌")) + #expect(topLine.hasSuffix("┐")) + #expect(bottomLine.hasPrefix("└")) + #expect(bottomLine.hasSuffix("┘")) + } + + @Test("BorderedView with doubleLine style uses correct characters") + func borderedViewDoubleLineStyle() { + let bordered = BorderedView(content: Text("X"), style: .doubleLine, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + let topLine = buffer.lines[0].stripped + let bottomLine = buffer.lines[buffer.height - 1].stripped + + #expect(topLine.hasPrefix("╔")) + #expect(topLine.hasSuffix("╗")) + #expect(bottomLine.hasPrefix("╚")) + #expect(bottomLine.hasSuffix("╝")) + } + + @Test("BorderedView with rounded style uses correct characters") + func borderedViewRoundedStyle() { + let bordered = BorderedView(content: Text("X"), style: .rounded, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + let topLine = buffer.lines[0].stripped + let bottomLine = buffer.lines[buffer.height - 1].stripped + + #expect(topLine.hasPrefix("╭")) + #expect(topLine.hasSuffix("╮")) + #expect(bottomLine.hasPrefix("╰")) + #expect(bottomLine.hasSuffix("╯")) + } + + @Test("BorderedView with heavy style uses correct characters") + func borderedViewHeavyStyle() { + let bordered = BorderedView(content: Text("X"), style: .heavy, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + let topLine = buffer.lines[0].stripped + let bottomLine = buffer.lines[buffer.height - 1].stripped + + #expect(topLine.hasPrefix("┏")) + #expect(topLine.hasSuffix("┓")) + #expect(bottomLine.hasPrefix("┗")) + #expect(bottomLine.hasSuffix("┛")) + } + + @Test("BorderedView with ascii style uses correct characters") + func borderedViewAsciiStyle() { + let bordered = BorderedView(content: Text("X"), style: .ascii, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + let topLine = buffer.lines[0].stripped + let bottomLine = buffer.lines[buffer.height - 1].stripped + + #expect(topLine.hasPrefix("+")) + #expect(topLine.hasSuffix("+")) + #expect(bottomLine.hasPrefix("+")) + #expect(bottomLine.hasSuffix("+")) + } + + @Test("BorderedView adds 2 to content width") + func borderedViewWidthOverhead() { + let bordered = BorderedView(content: Text("ABCDE"), style: .line, color: nil) + let context = testContext() + let buffer = bordered.renderToBuffer(context: context) + + // Content "ABCDE" = 5, + 2 for borders = 7 + let topLine = buffer.lines[0].stripped + #expect(topLine.count == 7) + } + + @Test("BorderedView conforms to Renderable") + func borderedViewIsRenderable() { + let bordered = BorderedView(content: Text("Test"), style: .line, color: nil) + #expect(bordered is Renderable) + } +} + +// MARK: - BorderStyle Tests + +@Suite("BorderStyle Tests") +struct BorderStyleTests { + + @Test("Predefined styles exist") + func predefinedStyles() { + // Just verify they all exist and have distinct characters + #expect(BorderStyle.line.topLeft == "┌") + #expect(BorderStyle.doubleLine.topLeft == "╔") + #expect(BorderStyle.rounded.topLeft == "╭") + #expect(BorderStyle.heavy.topLeft == "┏") + #expect(BorderStyle.block.topLeft == "▄") + #expect(BorderStyle.ascii.topLeft == "+") + #expect(BorderStyle.none.topLeft == " ") + } + + @Test("Line style has T-junctions") + func lineStyleTJunctions() { + #expect(BorderStyle.line.leftT == "├") + #expect(BorderStyle.line.rightT == "┤") + } + + @Test("DoubleLine style has T-junctions") + func doubleLineTJunctions() { + #expect(BorderStyle.doubleLine.leftT == "╠") + #expect(BorderStyle.doubleLine.rightT == "╣") + } + + @Test("Heavy style has T-junctions") + func heavyTJunctions() { + #expect(BorderStyle.heavy.leftT == "┣") + #expect(BorderStyle.heavy.rightT == "┫") + } + + @Test("BorderStyle is Equatable") + func borderStyleEquatable() { + #expect(BorderStyle.line == BorderStyle.line) + #expect(BorderStyle.line != BorderStyle.doubleLine) + } + + @Test("Custom border style stores all characters") + func customBorderStyle() { + let custom = BorderStyle( + topLeft: "A", + topRight: "B", + bottomLeft: "C", + bottomRight: "D", + horizontal: "E", + vertical: "F", + leftT: "G", + rightT: "H" + ) + #expect(custom.topLeft == "A") + #expect(custom.topRight == "B") + #expect(custom.bottomLeft == "C") + #expect(custom.bottomRight == "D") + #expect(custom.horizontal == "E") + #expect(custom.vertical == "F") + #expect(custom.leftT == "G") + #expect(custom.rightT == "H") + } + + @Test("Custom border style defaults T-junctions to vertical") + func customBorderStyleDefaultTJunctions() { + let custom = BorderStyle( + topLeft: "A", + topRight: "B", + bottomLeft: "C", + bottomRight: "D", + horizontal: "E", + vertical: "F" + ) + #expect(custom.leftT == "F") + #expect(custom.rightT == "F") + } + + @Test("Block style constants exist") + func blockStyleConstants() { + #expect(BorderStyle.blockBottomHorizontal == "▀") + #expect(BorderStyle.blockFooterSeparator == "▄") + } +} + +// MARK: - BackgroundModifier Tests + +@Suite("BackgroundModifier Tests") +struct BackgroundModifierTests { + + @Test("Background modifier applies ANSI background code") + func backgroundAppliesCode() { + let modifier = BackgroundModifier(color: .red) + let buffer = FrameBuffer(lines: ["Hello"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + #expect(result.height == 1) + // Should contain ANSI escape codes + let line = result.lines[0] + #expect(line.contains("\u{1B}[")) + #expect(line.contains("Hello")) + // Should end with reset + #expect(line.hasSuffix(ANSIRenderer.reset)) + } + + @Test("Background modifier preserves line count") + func backgroundPreservesLineCount() { + let modifier = BackgroundModifier(color: .blue) + let buffer = FrameBuffer(lines: ["Line 1", "Line 2", "Line 3"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + #expect(result.height == 3) + } + + @Test("Background modifier on empty buffer returns empty") + func backgroundEmptyBuffer() { + let modifier = BackgroundModifier(color: .green) + let buffer = FrameBuffer() + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + #expect(result.isEmpty) + } + + @Test("Background modifier pads lines to full width") + func backgroundPadsLines() { + let modifier = BackgroundModifier(color: .red) + let buffer = FrameBuffer(lines: ["Short", "VeryLongLine"]) + let context = testContext() + + let result = modifier.modify(buffer: buffer, context: context) + + // Both lines should have the same visible width after padding + #expect(result.lines[0].strippedLength == result.lines[1].strippedLength) + } +} diff --git a/Tests/TUIkitTests/TViewTests.swift b/Tests/TUIkitTests/ViewTests.swift similarity index 100% rename from Tests/TUIkitTests/TViewTests.swift rename to Tests/TUIkitTests/ViewTests.swift