JSPM

tui-tester

0.3.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 25
  • Score
    100M100P100Q44009F
  • License MIT

End-to-end testing framework for terminal user interfaces

Package Exports

  • tui-tester
  • tui-tester/adapters
  • tui-tester/helpers
  • tui-tester/integration/vitest
  • tui-tester/package.json
  • tui-tester/snapshot

Readme

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:

    # macOS
    brew install tmux
    
    # Ubuntu/Debian
    apt-get install tmux
    
    # Fedora
    dnf install tmux
  2. Install dependencies:

    npm install --save-dev vitest

๐ŸŽฏ Quick Start

Basic Example

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

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

// 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

// 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

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

// 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

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

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

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

// 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

# 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