npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

tui-tester

v0.3.0

Published

End-to-end testing framework for terminal user interfaces

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', '[email protected]');
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', '[email protected]');
        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