Skip to content

Install and run your first test

This tutorial starts a real terminal program, presses a key, and checks the rendered result. It does not require a framework integration.

  • Use Node.js 22 or 24. Other major versions are not supported.
  • You do not need to install Vitest separately. Termwright includes Vitest 4.1.11.

The native PTY package supports macOS 13.5+, Windows 10 1809+ or Server 2019+, and glibc 2.35+ Linux. Alpine/musl is not supported. See supported platforms for architectures and other limits.

Install the termwright package:

Terminal window
npm install --save-dev termwright

Then check that Termwright can load its test engine and native PTY backend:

Terminal window
npx termwright doctor

The command exits with code 0 when each required check passes.

Save this as app.mjs. The .mjs extension keeps the example self-contained and does not require changing your project’s module type.

import readline from 'node:readline';
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode?.(true);
process.stdout.write('Permission required\n[Approve] Reject\n');
process.stdin.once('keypress', (_input, key) => {
if (key.name === 'return') {
process.stdout.write('running: ls -la\n');
process.exit(0);
}
});

The program waits for Enter before printing its result. It uses raw input, so a plain redirected stdin/stdout test would not reproduce how it runs in a terminal.

Create tests/permission.test.ts:

import { fileURLToPath } from 'node:url';
import { expect, test } from 'termwright/test';
const program = fileURLToPath(new URL('../app.mjs', import.meta.url));
test('approves a command', async ({ terminal }) => {
const app = await terminal.launch({ command: [process.execPath, program] });
await app.waitForText('Permission required');
await app.press('Enter');
await expect(app).toHaveText('running: ls -la');
});

Run the test:

Terminal window
npx termwright test

The command reports one passing test and exits with code 0. Termwright closes the application and removes the test’s temporary working directory after the test completes.

A runnable version of this example is available in examples/getting-started.

Change the expected text to running: pwd and run the test again. The assertion waits for the configured timeout, reports the observed terminal state, and keeps a trace for the failed attempt.

Open the Runner:

Terminal window
npx termwright ui

The Runner starts in watch mode and will see the failed test in the current project. Select its failed attempt to inspect the terminal, steps, and replay timeline. Put the original expectation back when you are done.

Replace program with the command your users run. Keep command arguments as separate array items:

const app = await terminal.launch({
command: ['my-cli', 'deploy', '--environment', 'staging'],
});

Use text and keyboard APIs for a black-box test. If your application uses Ink, OpenTUI, Textual, tview, Bubble Tea, or Ratatui, follow its integration guide before using controls by role or label. With an integration enabled:

const approve = app.getByRole('button', { name: 'Approve' });
await expect(approve).toBeAttached();
await app.press('Enter');
await expect(app.getByRole('status')).toHaveText('Approved');