tui-tester
Version:
End-to-end testing framework for terminal user interfaces
496 lines (397 loc) โข 11.6 kB
Markdown
# Terminal E2E Testing Framework
A comprehensive system for end-to-end testing of terminal applications with support for mouse, keyboard, snapshots, and session recording.
## ๐ Features
- โ
**Cross-platform**: Works with Node.js, Deno, and Bun
- โ
**Real testing**: Uses tmux to create an actual terminal
- โ
**Fully asynchronous**: All operations are non-blocking
- โ
**Mouse control**: Clicks, drag and drop, scrolling
- โ
**Keyboard control**: All keys and modifiers
- โ
**Snapshot system**: Save and compare screen states
- โ
**Recording and playback**: Record user actions
- โ
**Test framework integration**: Vitest, Jest
- โ
**High-level utilities**: Ready-made functions for common actions
## ๐ฆ Installation
### Requirements
1. **tmux** must be installed on the system:
```bash
# macOS
brew install tmux
# Ubuntu/Debian
apt-get install tmux
# Fedora
dnf install tmux
```
2. Install dependencies:
```bash
npm install --save-dev vitest
```
## ๐ฏ Quick Start
### Basic Example
```typescript
import { createTester } from 'tui-tester';
async function testMyApp() {
const tester = createTester('node app.js', {
cols: 80,
rows: 24,
debug: true
});
await tester.start();
// Wait for app to load
await tester.waitForText('Welcome');
// Type text
await tester.typeText('Hello, World!');
await tester.sendKey('enter');
// Check result
await tester.assertScreenContains('Hello, World!');
// Take snapshot
await tester.takeSnapshot('hello-world');
await tester.stop();
}
```
### Using Vitest
```typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { TmuxTester } from 'tui-tester';
import { setupVitestMatchers } from 'tui-tester/integrations/vitest';
// Setup custom matchers
setupVitestMatchers();
describe('My TUI App', () => {
let tester: TmuxTester;
beforeAll(async () => {
tester = new TmuxTester({
command: ['npm', 'start'],
size: { cols: 120, rows: 40 }
});
await tester.start();
});
afterAll(async () => {
await tester.stop();
});
it('should match snapshot', async () => {
await tester.waitForText('Ready');
const screen = await tester.captureScreen();
await expect(screen).toMatchTerminalSnapshot('main-screen');
});
});
```
## ๐ฎ Keyboard Control
```typescript
// Regular keys
await tester.sendText('Hello');
await tester.sendKey('enter');
await tester.sendKey('tab');
await tester.sendKey('escape');
// Special keys
await tester.sendKey('up');
await tester.sendKey('down');
await tester.sendKey('left');
await tester.sendKey('right');
await tester.sendKey('home');
await tester.sendKey('end');
await tester.sendKey('pageup');
await tester.sendKey('pagedown');
// With modifiers
await tester.sendKey('c', { ctrl: true }); // Ctrl+C
await tester.sendKey('v', { ctrl: true }); // Ctrl+V
await tester.sendKey('a', { ctrl: true }); // Ctrl+A
await tester.sendKey('tab', { shift: true }); // Shift+Tab
await tester.sendKey('f', { alt: true }); // Alt+F
await tester.sendKey('s', { ctrl: true, shift: true }); // Ctrl+Shift+S
// Function keys
await tester.sendKey('f1');
await tester.sendKey('f12');
// Type text with delay
await tester.typeText('Typing slowly...', 100); // 100ms between characters
```
## ๐ฑ๏ธ Mouse Control
```typescript
// Clicks
await tester.sendMouse({
type: 'click',
position: { x: 10, y: 5 },
button: 'left'
});
// Right click
await tester.sendMouse({
type: 'click',
position: { x: 20, y: 10 },
button: 'right'
});
// Drag and drop
await tester.sendMouse({
type: 'down',
position: { x: 10, y: 10 },
button: 'left'
});
await tester.sendMouse({
type: 'drag',
position: { x: 30, y: 20 },
button: 'left'
});
await tester.sendMouse({
type: 'up',
position: { x: 30, y: 20 },
button: 'left'
});
// Scrolling
await tester.sendMouse({
type: 'scroll',
position: { x: 50, y: 25 },
button: 'up' // or 'down'
});
```
## ๐ธ Snapshots
```typescript
import { SnapshotManager } from 'tui-tester';
const snapshotManager = new SnapshotManager({
updateSnapshots: process.env.UPDATE_SNAPSHOTS === 'true',
snapshotDir: './__snapshots__',
format: 'json' // or 'text', 'ansi'
});
// Create snapshot
const capture = await tester.captureScreen();
const result = await snapshotManager.matchSnapshot(
capture,
'my-snapshot-name'
);
if (!result.pass) {
console.log('Snapshot mismatch:', result.diff);
}
// Update snapshots
// Run tests with UPDATE_SNAPSHOTS=true to update
```
## ๐ฌ Recording and Playback
```typescript
// Start recording
tester.startRecording();
// Perform actions
await tester.sendText('echo "Recording test"');
await tester.sendKey('enter');
// Stop recording
const recording = tester.stopRecording();
// Save recording (using Node.js fs as example)
import { writeFileSync, readFileSync } from 'fs';
writeFileSync('recording.json', JSON.stringify(recording));
// Load and replay
const savedRecording = JSON.parse(
readFileSync('recording.json', 'utf-8')
);
await tester.playRecording(savedRecording, 2); // 2x speed
```
## ๐ ๏ธ High-level Utilities
```typescript
import {
navigateMenu,
selectMenuItem,
fillField,
submitForm,
clickOnText,
selectText,
copySelection,
pasteFromClipboard,
waitForLoading,
login,
executeCommand,
search
} from 'tui-tester';
// Menu navigation
await navigateMenu(tester, 'down', 3);
await selectMenuItem(tester, 'Settings');
// Fill form
await fillField(tester, 'Username', 'john.doe');
await fillField(tester, 'Email', 'john@example.com');
await submitForm(tester);
// Text operations
await clickOnText(tester, 'Click me!');
await selectText(tester, 'start', 'end');
await copySelection(tester);
await pasteFromClipboard(tester);
// Wait for loading
await waitForLoading(tester);
// Login
await login(tester, 'username', 'password');
// Execute command
await executeCommand(tester, 'ls -la');
// Search
await search(tester, 'search term');
```
## ๐งช Test Scenarios
```typescript
import { TestRunner, scenario, step } from 'tui-tester';
const runner = new TestRunner({
timeout: 30000,
retries: 2,
debug: true
});
const testScenario = scenario(
'User Registration Flow',
[
step(
'Navigate to registration',
async (t) => {
await selectMenuItem(t, 'Register');
await t.waitForText('Registration Form');
},
async (t) => {
await t.assertScreenContains('Create Account');
}
),
step(
'Fill registration form',
async (t) => {
await fillField(t, 'Username', 'newuser');
await fillField(t, 'Email', 'user@example.com');
await fillField(t, 'Password', 'secure123');
}
),
step(
'Submit and verify',
async (t) => {
await submitForm(t);
await t.waitForText('Registration successful');
},
async (t) => {
await t.assertScreenContains('Welcome, newuser');
}
)
],
{
setup: async () => {
console.log('Setting up test environment');
},
teardown: async () => {
console.log('Cleaning up');
}
}
);
const result = await runner.runScenario(testScenario, {
command: ['node', 'app.js'],
size: { cols: 80, rows: 24 }
});
runner.printResults();
```
## ๐ง Configuration
```typescript
interface TesterConfig {
command: string[]; // Command to run the application
size?: TerminalSize; // Terminal size (default 80x24)
env?: Record<string, string>; // Environment variables
cwd?: string; // Working directory
shell?: string; // Shell (default 'sh')
sessionName?: string; // tmux session name
debug?: boolean; // Debug mode
recordingEnabled?: boolean; // Automatic recording
snapshotDir?: string; // Snapshot directory
}
```
## ๐ Debugging
```typescript
// Enable debug mode
const tester = new TmuxTester({
command: ['node', 'app.js'],
debug: true // Outputs all commands and results
});
// Capture screen on error
try {
await tester.waitForText('Expected');
} catch (error) {
const screen = await tester.captureScreen();
console.log('Screen at error:', screen.text);
throw error;
}
// Check status
console.log('Is running:', tester.isRunning());
console.log('Size:', tester.getSize());
```
## ๐ CI/CD Integration
```yaml
# GitHub Actions
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install tmux
run: sudo apt-get install -y tmux
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run E2E tests
run: npm run test:e2e
env:
CI: true
```
## ๐ API Reference
### TmuxTester
Main class for testing terminal applications.
#### Lifecycle Methods
- `start()` - Start application
- `stop()` - Stop application
- `restart()` - Restart
- `isRunning()` - Check status
#### Input
- `sendText(text)` - Send text
- `sendKey(key, modifiers?)` - Send key
- `sendKeys(keys[])` - Send multiple keys
- `sendMouse(event)` - Send mouse event
- `typeText(text, delay?)` - Type with delay
- `paste(text)` - Paste text
- `sendCommand(command)` - Send command to tmux
#### Mouse
- `enableMouse()` - Enable mouse support
- `disableMouse()` - Disable mouse support
- `click(x, y)` - Click at position
- `clickText(text)` - Click on text
- `doubleClick(x, y)` - Double-click
- `rightClick(x, y)` - Right-click
- `drag(from, to)` - Drag and drop
- `scroll(direction, lines?)` - Scroll
#### Output
- `captureScreen()` - Capture screen
- `getScreenText()` - Get text without ANSI
- `getScreenLines()` - Get lines array
- `getScreen(options?)` - Get screen with options
- `getScreenContent()` - Alias for getScreenText
- `getLines()` - Get screen lines
- `waitForText(text, options?)` - Wait for text
- `waitForPattern(regex, options?)` - Wait for pattern
- `waitForLine(lineNumber, text, options?)` - Wait for specific line
#### Assertions
- `assertScreen(expected, options?)` - Assert screen
- `assertScreenContains(text, options?)` - Assert text presence
- `assertScreenMatches(pattern, options?)` - Assert by pattern
- `assertLine(lineNumber, predicate)` - Assert specific line
- `assertCursorAt(position)` - Assert cursor position
#### Cursor
- `getCursor()` - Get cursor position
- `assertCursorAt(position)` - Assert cursor position
#### Snapshots
- `takeSnapshot(name?)` - Create snapshot
- `compareSnapshot(snapshot)` - Compare with snapshot
- `saveSnapshot(snapshot, path?)` - Save snapshot to file
- `loadSnapshot(path)` - Load snapshot from file
#### Recording
- `startRecording()` - Start recording session
- `stopRecording()` - Stop and return recording
- `playRecording(recording, speed?)` - Replay recording
#### Utilities
- `clear()` - Clear screen
- `reset()` - Reset terminal
- `resize(size)` - Resize terminal
- `getSize()` - Get terminal size
- `getSessionName()` - Get tmux session name
- `getLastOutput()` - Get last captured output
- `clearOutput()` - Clear output buffer
- `exec(command)` - Execute command and get result
- `capture()` - Enhanced capture with cursor
- `sleep(ms)` - Sleep for milliseconds
- `debug(message)` - Output debug message
## ๐ค Contributing
We welcome contributions to the project! Please create an issue or pull request.
## ๐ License
MIT