tuix
Version:
A performant TUI framework for Bun with JSX and reactive state management
283 lines (236 loc) ⢠9.87 kB
text/typescript
/**
* Visual Testing - Comprehensive tests for rendering accuracy
*/
import { Effect } from "effect"
import * as View from "@/core/view.ts"
import { stringWidth } from "@/utils/string-width.ts"
/**
* Test individual view components and their rendered output
*/
export async function testViewRendering() {
console.log("šØ Testing View Rendering Accuracy\n")
// Test 1: Basic text rendering
console.log("š Test 1: Basic Text")
const text1 = View.text("Count: 123")
const rendered1 = await Effect.runPromise(text1.render())
console.log(`Input: "Count: 123"`)
console.log(`Output: "${rendered1}"`)
console.log(`Width: ${text1.width}, Expected: ${stringWidth("Count: 123")}`)
console.log(`Match: ${rendered1 === "Count: 123" ? "ā
" : "ā"}\n`)
// Test 2: Emoji text rendering
console.log("š Test 2: Emoji Text")
const text2 = View.text("šÆ Counter App")
const rendered2 = await Effect.runPromise(text2.render())
console.log(`Input: "šÆ Counter App"`)
console.log(`Output: "${rendered2}"`)
console.log(`Width: ${text2.width}, Expected: ${stringWidth("šÆ Counter App")}`)
console.log(`Match: ${rendered2 === "šÆ Counter App" ? "ā
" : "ā"}\n`)
// Test 3: VStack rendering
console.log("š Test 3: VStack")
const vstack = View.vstack(
View.text("šÆ Counter App"),
View.text(""),
View.text("Count: 0"),
View.text("Last: Started")
)
const rendered3 = await Effect.runPromise(vstack.render())
const expected3 = "šÆ Counter App\n\nCount: 0\nLast: Started"
console.log(`VStack output:`)
console.log(`"${rendered3.replace(/\n/g, '\\n')}"`)
console.log(`Expected:`)
console.log(`"${expected3.replace(/\n/g, '\\n')}"`)
console.log(`Width: ${vstack.width}, Height: ${vstack.height}`)
console.log(`Match: ${rendered3 === expected3 ? "ā
" : "ā"}\n`)
// Test 4: Box rendering
console.log("š Test 4: Box Rendering")
const content = View.vstack(
View.text("šÆ Counter App"),
View.text(""),
View.text("Count: 0"),
View.text("Last: Started")
)
const boxed = View.box(content)
const rendered4 = await Effect.runPromise(boxed.render())
console.log(`Box output:`)
console.log(rendered4)
console.log(`Box width: ${boxed.width}, height: ${boxed.height}`)
// Validate box structure
const lines = rendered4.split('\n')
console.log(`Number of lines: ${lines.length}`)
// Check each line
lines.forEach((line, i) => {
const lineWidth = stringWidth(line)
console.log(`Line ${i}: width=${lineWidth}, content="${line}"`)
})
// Test for box integrity
const hasTopBorder = lines[0].startsWith('ā') && lines[0].endsWith('ā')
const hasBottomBorder = lines[lines.length - 1].startsWith('ā') && lines[lines.length - 1].endsWith('ā')
const hasLeftBorder = lines.slice(1, -1).every(line => line.startsWith('ā'))
const hasRightBorder = lines.slice(1, -1).every(line => line.endsWith('ā'))
console.log(`Box integrity:`)
console.log(` Top border: ${hasTopBorder ? "ā
" : "ā"}`)
console.log(` Bottom border: ${hasBottomBorder ? "ā
" : "ā"}`)
console.log(` Left border: ${hasLeftBorder ? "ā
" : "ā"}`)
console.log(` Right border: ${hasRightBorder ? "ā
" : "ā"}\n`)
// Test 5: Styled text
console.log("š Test 5: Styled Text")
const bold = View.bold(View.text("Count: 123"))
const dim = View.dim(View.text("Last: Started"))
const rendered5a = await Effect.runPromise(bold.render())
const rendered5b = await Effect.runPromise(dim.render())
console.log(`Bold: "${rendered5a}"`)
console.log(`Dim: "${rendered5b}"`)
console.log(`Bold width: ${bold.width}, Dim width: ${dim.width}`)
// Check if ANSI codes are present
const hasBoldAnsi = rendered5a.includes('\x1b[1m') && rendered5a.includes('\x1b[0m')
const hasDimAnsi = rendered5b.includes('\x1b[2m') && rendered5b.includes('\x1b[0m')
console.log(`Bold ANSI: ${hasBoldAnsi ? "ā
" : "ā"}`)
console.log(`Dim ANSI: ${hasDimAnsi ? "ā
" : "ā"}\n`)
// Test 6: Center alignment
console.log("š Test 6: Center Alignment")
const centered = View.center(View.text("Test"), 20)
const rendered6 = await Effect.runPromise(centered.render())
console.log(`Centered in 20 chars: "${rendered6}"`)
console.log(`Length: ${rendered6.length}, Expected: 20`)
console.log(`Centered correctly: ${rendered6.length === 20 ? "ā
" : "ā"}\n`)
}
/**
* Test the complete counter component rendering
*/
export async function testCounterRendering() {
console.log("š§® Testing Counter Component Rendering\n")
// Import the counter component
const { CounterComponent } = await import("../../examples/counter.ts")
// Test initial state
const [initialModel, _] = await Effect.runPromise(CounterComponent.init)
const initialView = CounterComponent.view(initialModel)
const rendered = await Effect.runPromise(initialView.render())
console.log("Counter initial render:")
console.log(rendered)
console.log()
// Test after increment
const [incrementedModel, __] = await Effect.runPromise(
CounterComponent.update({ _tag: "Increment" }, initialModel)
)
const incrementedView = CounterComponent.view(incrementedModel)
const incrementedRendered = await Effect.runPromise(incrementedView.render())
console.log("Counter after increment:")
console.log(incrementedRendered)
console.log()
// Analyze the structure
const lines = rendered.split('\n')
console.log("Analysis:")
console.log(`Total lines: ${lines.length}`)
lines.forEach((line, i) => {
console.log(`Line ${i}: width=${stringWidth(line)}, "${line}"`)
})
}
/**
* Test the complete renderer pipeline
*/
export async function testRendererPipeline() {
console.log("š§ Testing Complete Renderer Pipeline\n")
const { Effect, Layer, Ref } = await import("effect")
const { RendererServiceLive } = await import("../services/impl/renderer-impl.ts")
const { TerminalServiceLive } = await import("../services/impl/terminal-impl.ts")
// Create a mock terminal buffer to capture output
class MockTerminal {
buffer: string[] = []
x = 1
y = 1
moveCursor = (x: number, y: number) =>
Effect.sync(() => {
this.x = x
this.y = y
this.buffer.push(`MOVE(${x},${y})`)
})
write = (text: string) =>
Effect.sync(() => {
this.buffer.push(`WRITE("${text}")`)
})
getSize = Effect.succeed({ width: 80, height: 24 })
clear = Effect.sync(() => {
this.buffer = []
})
hideCursor = Effect.void
showCursor = Effect.void
enableAltBuffer = Effect.void
disableAltBuffer = Effect.void
enableRawMode = Effect.void
disableRawMode = Effect.void
}
const mockTerminal = new MockTerminal()
// Create mock terminal service layer
const MockTerminalLayer = Layer.succeed({
moveCursor: mockTerminal.moveCursor,
write: mockTerminal.write,
getSize: mockTerminal.getSize,
clear: mockTerminal.clear,
hideCursor: mockTerminal.hideCursor,
showCursor: mockTerminal.showCursor,
enableAltBuffer: mockTerminal.enableAltBuffer,
disableAltBuffer: mockTerminal.disableAltBuffer,
enableRawMode: mockTerminal.enableRawMode,
disableRawMode: mockTerminal.disableRawMode
})
try {
const program = Effect.gen(function* (_) {
const renderer = yield* _({ RendererService: RendererServiceLive })
// Test 1: Simple text rendering
console.log("š Test 1: Simple text through renderer")
yield* _(renderer.beginFrame)
yield* _(renderer.renderAt(View.text("Hello World"), 5, 5))
yield* _(renderer.endFrame)
console.log("Terminal commands:")
mockTerminal.buffer.forEach(cmd => console.log(` ${cmd}`))
console.log()
// Test 2: Styled text rendering
console.log("š Test 2: Styled text through renderer")
mockTerminal.buffer = []
yield* _(renderer.beginFrame)
yield* _(renderer.renderAt(View.bold(View.text("Bold Text")), 10, 10))
yield* _(renderer.endFrame)
console.log("Terminal commands:")
mockTerminal.buffer.forEach(cmd => console.log(` ${cmd}`))
console.log()
// Test 3: Box rendering
console.log("š Test 3: Box through renderer")
mockTerminal.buffer = []
yield* _(renderer.beginFrame)
const boxView = View.box(View.text("Test"))
yield* _(renderer.renderAt(boxView, 0, 0))
yield* _(renderer.endFrame)
console.log("Terminal commands:")
mockTerminal.buffer.forEach(cmd => console.log(` ${cmd}`))
console.log()
// Verify no literal ANSI codes in output
const hasLiteralAnsi = mockTerminal.buffer.some(cmd =>
cmd.includes('WRITE("[1m') || cmd.includes('WRITE("[0m') ||
cmd.includes('WRITE("[2m'))
console.log(`No literal ANSI in output: ${!hasLiteralAnsi ? "ā
" : "ā"}`)
// Verify proper ANSI escape sequences
const hasProperAnsi = mockTerminal.buffer.some(cmd =>
cmd.includes('WRITE("\\x1b[1m') || cmd.includes('WRITE("\\x1b[0m'))
console.log(`Proper ANSI sequences: ${hasProperAnsi ? "ā
" : "ā"}`)
})
const layer = Layer.provide(MockTerminalLayer, RendererServiceLive)
await Effect.runPromise(Effect.provide(program, layer))
} catch (error) {
console.error("ā Renderer pipeline test failed:", error)
}
}
/**
* Run all visual tests
*/
export async function runVisualTests() {
try {
await testViewRendering()
console.log("ā".repeat(60))
await testCounterRendering()
console.log("ā".repeat(60))
await testRendererPipeline()
console.log("\nšÆ Visual tests completed!")
} catch (error) {
console.error("ā Visual tests failed:", error)
}
}