@unblessed/vrt
Version:
Visual Regression Testing tools for @unblessed terminal UI applications
419 lines (309 loc) • 9.33 kB
Markdown
Visual Regression Testing tools for @unblessed terminal UI applications.
`@unblessed/vrt` provides infrastructure for recording, playing back, and comparing terminal UI screenshots to detect visual regressions. It captures the raw SGR-encoded terminal output from `screen.screenshot()` and allows you to verify that UI rendering remains consistent across code changes.
```bash
npm install --save-dev @unblessed/vrt
pnpm add -D @unblessed/vrt
```
The `vrt` command provides tools for working with VRT recordings directly from the terminal.
```bash
vrt play recording.vrt.json
vrt play recording.vrt.json --speed 2.0
```
Plays the recording to your terminal, showing exactly what was captured.
```bash
vrt info recording.vrt.json
```
Displays metadata about the recording including dimensions, duration, frame count, and timestamps.
```bash
vrt compare expected.vrt.json actual.vrt.json
vrt compare expected.vrt.json actual.vrt.json \
--threshold 5 \
--ignore-colors \
--verbose
```
**Options:**
- `-t, --threshold <number>` - Allow N character differences (default: 0)
- `--ignore-colors` - Ignore ANSI color code differences
- `--ignore-whitespace` - Ignore whitespace differences
- `-v, --verbose` - Show detailed diff information
#### `vrt export` - Export to image formats
```bash
vrt export recording.vrt.json output.gif # Animated GIF
vrt export recording.vrt.json output.png --frame 0 # Single frame PNG
```
_Note: Export functionality coming soon_
### Usage in Scripts
Add CLI commands to your `package.json` scripts:
```json
{
"scripts": {
"vrt:info": "vrt info tests/fixtures/golden.vrt.json",
"vrt:compare": "vrt compare tests/fixtures/golden.vrt.json tests/fixtures/current.vrt.json"
}
}
```
The most common use case is golden snapshot testing with the `compareWithGolden` utility:
```typescript
import { compareWithGolden } from "@unblessed/vrt";
import { Screen, Box } from "@unblessed/node";
it("box renders correctly", async () => {
const screen = new Screen();
const box = new Box({
parent: screen,
content: "Hello",
border: { type: "line" },
});
screen.render();
// Capture screenshot
const recording = {
version: "1.0.0",
dimensions: { cols: screen.cols, rows: screen.rows },
metadata: {
createdAt: new Date().toISOString(),
duration: 0,
frameCount: 1,
description: "box test",
},
frames: [{ screenshot: screen.screenshot(), timestamp: 0 }],
};
// Compare with golden snapshot
const result = compareWithGolden(
"__tests__/fixtures/box.vrt.json",
recording,
"box renders correctly",
);
if (!result.pass) {
throw new Error(result.errorMessage);
}
screen.destroy();
});
```
**Commands:**
```bash
pnpm test
UPDATE_SNAPSHOTS=1 pnpm test
```
Complete golden snapshot workflow that handles:
- Creating golden on first run
- Updating golden when `UPDATE_SNAPSHOTS=1`
- Comparing with golden on normal runs
- Formatting detailed error messages
**Returns:** `GoldenComparisonResult`
- `pass: boolean` - Whether test should pass
- `action: 'created' | 'updated' | 'matched' | 'failed'`
- `errorMessage?: string` - Formatted error (on failure)
#### `saveGoldenSnapshot(path, recording)`
Save a VRT recording as a golden snapshot file.
### Recording & Playback
### Recording a Session
```typescript
import { VRTRecorder } from "@unblessed/vrt";
import { Screen, Box } from "@unblessed/node";
const screen = new Screen({ smartCSR: true });
const recorder = new VRTRecorder(screen, {
interval: 100, // Capture every 100ms
outputPath: "./recording.vrt.json",
});
// Start recording
recorder.start();
// Create UI and interact
const box = new Box({
parent: screen,
content: "Hello World",
border: { type: "line" },
});
screen.render();
// Stop and save
const recording = recorder.stop();
// Saved to ./recording.vrt.json
```
```typescript
import { VRTPlayer } from "@unblessed/vrt";
const player = new VRTPlayer("./recording.vrt.json");
// Play to stdout
await player.play({ writeToStdout: true });
// Process each frame
await player.play({
onFrame: (frame, index) => {
console.log(`Frame ${index}: ${frame.screenshot.length} bytes`);
},
speed: 2.0, // 2x speed
});
```
```typescript
import { VRTComparator } from "@unblessed/vrt";
const result = VRTComparator.compare(
"./golden.vrt.json", // Expected
"./current.vrt.json", // Actual
{
threshold: 5, // Allow up to 5 char differences
ignoreColors: false, // Compare colors too
},
);
if (!result.match) {
console.error(`Visual regression detected!`);
console.error(
` ${result.differentFrames} of ${result.totalFrames} frames differ`,
);
result.differences?.forEach((diff) => {
console.error(
` Frame ${diff.frameIndex}: ${diff.diffCount} chars different`,
);
});
}
```
Use VRT in your Vitest tests:
```typescript
import { describe, it, expect } from "vitest";
import { Screen, Box } from "@unblessed/node";
import { VRTRecorder, VRTComparator } from "@unblessed/vrt";
describe("Box rendering", () => {
it("renders with border correctly", async () => {
const screen = new Screen();
const recorder = new VRTRecorder(screen);
recorder.start();
const box = new Box({
parent: screen,
top: 0,
left: 0,
width: 20,
height: 5,
content: "Test",
border: { type: "line" },
});
screen.render();
await new Promise((resolve) => setTimeout(resolve, 100));
const recording = recorder.stop();
screen.destroy();
// Compare with golden snapshot
const result = VRTComparator.compare(
"./__tests__/fixtures/box-golden.vrt.json",
recording,
);
expect(result.match).toBe(true);
});
});
```
Records screen screenshots at regular intervals.
**Constructor:**
```typescript
new VRTRecorder(screen: Screen, options?: VRTRecorderOptions)
```
**Methods:**
- `start()` - Start recording
- `stop()` - Stop recording and return VRTRecording
- `isRecording()` - Check if recording is in progress
- `getFrameCount()` - Get current frame count
### VRTPlayer
Plays back VRT recordings.
**Constructor:**
```typescript
new VRTPlayer(source: string | VRTRecording)
```
**Methods:**
- `play(options?: VRTPlayerOptions)` - Play the recording
- `getFrames()` - Get all frames
- `getFrame(index)` - Get specific frame
- `getMetadata()` - Get recording metadata
- `getDimensions()` - Get terminal dimensions
### VRTComparator
Compares two VRT recordings.
**Static Methods:**
- `compare(expected, actual, options?)` - Compare two recordings
- `compareRecordings(expected, actual, options?)` - Compare VRTRecording objects
## Types
```typescript
interface VRTRecorderOptions {
interval?: number;
outputPath?: string;
description?: string;
metadata?: Record<string, any>;
}
interface VRTPlayerOptions {
speed?: number;
onFrame?: (frame: VRTFrame, index: number) => void;
writeToStdout?: boolean;
}
interface VRTComparatorOptions {
threshold?: number;
ignoreColors?: boolean;
ignoreWhitespace?: boolean;
}
interface VRTComparisonResult {
match: boolean;
totalFrames: number;
matchedFrames: number;
differentFrames: number;
differentFrameIndices: number[];
differences?: VRTFrameDifference[];
}
```
VRT recordings are JSON files with this structure:
```json
{
"version": "1.0.0",
"dimensions": {
"cols": 80,
"rows": 24
},
"metadata": {
"createdAt": "2025-10-21T...",
"duration": 1000,
"frameCount": 10,
"description": "Test recording"
},
"frames": [
{
"screenshot": "\u001b[H\u001b[2J...",
"timestamp": 0
}
]
}
```
The `vrt export` command is planned to support exporting VRT recordings to image formats:
**Planned Features:**
- **PNG Export**: Export individual frames as static PNG images
- Useful for documentation, GitHub PRs, and CI reports
- Renders ANSI escape codes to pixel buffers
- Uses `pngjs` library (already included)
- **GIF Export**: Export entire recordings as animated GIFs
- Perfect for showcasing UI interactions
- Configurable frame rate and speed
- Uses `omggif` library (already included)
**Implementation Requirements:**
- ANSI-to-image renderer that converts terminal escape codes to pixel buffers
- Font rendering (monospace font required)
- Color palette mapping (256-color and true color support)
- Text rendering with proper character spacing
**Potential Extensions:**
- SVG export for scalable terminal output
- HTML export with interactive playback controls
- Video export (MP4/WebM) for high-quality recordings
- Side-by-side diff visualization
## License
MIT