Skip to content

termlessHeadless terminal testing

Like Playwright, but for terminal apps. Write tests once, run against any backend. ~1ms per test.

Quick Start

bash
bun add -d @termless/test
typescript
import { test, expect } from "vitest"
import { createTerminalFixture } from "@termless/test"

// ANSI helpers — real apps use @hightea/term or @hightea/ansi, these are just for test data
const BOLD = (s: string) => `\x1b[1m${s}\x1b[0m`
const GREEN = (s: string) => `\x1b[38;2;0;255;0m${s}\x1b[0m`

test("inspect what string matching can't see", () => {
  // Creates an xterm.js terminal by default. Ghostty, Alacritty, WezTerm, vt100,
  // and Peekaboo backends are also available — see Multi-Backend Testing below.
  const term = createTerminalFixture({ cols: 40, rows: 3 })

  // Simulate a build pipeline — 4 lines overflow a 3-row terminal
  term.feed("Step 1: install\r\n")
  term.feed(`Step 2: ${GREEN("build ok")}\r\n`)
  term.feed(`Step 3: ${BOLD("test")}\r\n`)
  term.feed("Step 4: deploy")

  // Region selectors — screen, scrollback, buffer
  expect(term.scrollback).toContainText("install") // scrolled off, still in history
  expect(term.screen).toContainText("deploy") // visible area
  expect(term.buffer).toContainText("install") // everything (scrollback + screen)
  expect(term.row(0)).toHaveText("Step 2: build ok") // specific row

  // Cell styles — colors that getText() can't see
  expect(term.cell(0, 8)).toHaveFg("#00ff00") // "build ok" is green
  expect(term.cell(1, 8)).toBeBold() // "test" is bold

  // Scroll up, then assert on viewport
  term.backend.scrollViewport(1)
  expect(term.viewport).toContainText("install")

  // Resize — verify content survives
  term.resize(20, 3)
  expect(term.screen).toContainText("deploy")

  // Terminal state — window title, cursor, modes
  term.feed("\x1b]2;Build Pipeline\x07") // OSC 2 — set window title
  expect(term).toHaveTitle("Build Pipeline")
  expect(term).toHaveCursorAt(14, 2) // after "Step 4: deploy"
  expect(term).toBeInMode("autoWrap") // default mode
  expect(term).not.toBeInMode("altScreen") // not in alternate screen
})

Why Not Just Assert on Strings?

String assertions on terminal output break constantly:

  • ANSI codes make string matching fragile (\x1b[1m litters your test)
  • Trailing whitespace differs between terminals and runs
  • Wide characters (emoji, CJK) occupy 2 columns but 1 string position
  • Colors are invisible in getText() — a red error looks the same as a green success

termless gives you structured access to the terminal buffer. Assert on what matters:

typescript
// Instead of fragile string matching...
expect(output).toContain("\x1b[1;31mError\x1b[0m")

// ...assert on terminal state
expect(term.screen).toContainText("Error")
expect(term.cell(0, 0)).toBeBold()
expect(term.cell(0, 0)).toHaveFg("#ff0000")
expect(term).toBeInMode("altScreen")
expect(term).toHaveTitle("my-app")

Packages

PackageDescription
termlessCore: Terminal API, PTY, SVG/PNG screenshots, key mapping, region views
@termless/xtermjsxterm.js backend via @xterm/headless
@termless/ghosttyGhostty backend via ghostty-web WASM
@termless/vt100Pure TypeScript VT100 emulator, zero native deps
@termless/alacrittyAlacritty backend via alacritty_terminal (napi-rs)
@termless/weztermWezTerm backend via wezterm-term (napi-rs)
@termless/peekabooOS-level terminal automation (xterm.js + real app)
@termless/testVitest integration: 25+ matchers, fixtures, snapshot serializer
@termless/cliCLI tools + MCP server for AI agents

How It Compares

FeaturetermlessManual string testingPlaywright
Speed~1ms/test~1ms/test~100ms+/test
Terminal internalsScrollback, cursor, modes, cell attrsNoneN/A
ANSI awarenessFull (colors, bold, cursor)NoneN/A
Multi-backend6 terminal emulatorsN/A3 browsers
Protocol capabilitiesKitty, sixel, OSC 8, reflowNoneN/A
Wide char supportCell-level width trackingBrokenN/A
ScreenshotsSVG + PNG (no Chromium)NonePNG (Chromium)
PTY supportSpawn real processesManualN/A
AI integrationMCP serverNoneNone

Released under the MIT License.