Skip to content

Test Ink components

Use mountInk() for normal Ink component tests. Use launchInkFixture() when the component’s process, environment, signals, crash behavior, or real PTY is part of the test.

Terminal window
npm install --save-dev termwright
import { mountInk } from 'termwright/ink';
import { expect, test, vi } from 'termwright/test';
import { Approve } from './Approve.js';
test('approves the request', async ({ terminal }) => {
const onApprove = vi.fn();
const harness = await terminal.attach(await mountInk(<Approve onApprove={onApprove} />), {
command: ['<mountInk>'],
});
await harness.press('Tab');
await harness.press('Enter');
await vi.waitFor(() => expect(onApprove).toHaveBeenCalledOnce());
});

Input is terminal bytes. No helper invokes component callbacks directly. terminal.attach() adds the component session to traces, Runner live state, logs, and test teardown. Call mountInk() directly only when a standalone Vitest test deliberately owns and closes the harness itself.

Peer dependencies are Ink 7.1.1 and React >= 19.2. A vanilla component is observable without an application import. Add optional useSemantic or <Semantic> from @termwright/ink where the retained host tree lacks application intent.

mountInklaunchInkFixture
where it runscurrent test processchild process in a real PTY
propsany React props, including spiesbounded JSON
rerenderReact elementJSON props
process/env/signal fidelitymodelledreal

Use mountInk for component behavior and launchInkFixture when process identity, environment, signals, crash reporting, or a real PTY is part of the contract. Both return TerminalHarness, so locators, screen assertions, input, waits, traces, and snapshots are the same.

Both modes wait for the first rendered state before they resolve. When one key changes application state needed by the next key, send them separately:

await harness.press('Tab');
await harness.press('Enter');

Ink exposes component layout and the part visible in the viewport. Locator clicks additionally require the component to register its real pointer router; without one, use keyboard input. Termwright still sends pointer input through the component harness instead of calling a callback directly.

@termwright/ink is the focused package behind termwright/ink. Install it directly when a component-only project deliberately wants the focused harness without the Termwright CLI and Runner dependencies.

A fixture module default-exports the component. Props are JSON values sent over the fixture control channel, not through stdin:

const harness = await launchInkFixture({
component: new URL('./approve-fixture.mjs', import.meta.url),
props: { label: 'Approve' },
nodeArgs: ['--import', 'tsx'],
columns: 40,
rows: 10,
});
await harness.rerender({ label: 'Reject' });

The child receives Termwright’s isolated test environment. An in-process mount does not change the test process’s process.env, global console, stdin, or stdout. Use a child fixture when console or process environment is what the test needs to observe.

A static fixture must keep its event loop alive long enough for the probe handshake. Interactive components already do this through useInput.

  • mountInk(element, options?) returns InkHarness, adding rerender(element) and renderError() to TerminalHarness.
  • launchInkFixture(options) returns InkFixtureHarness, adding rerender(jsonProps).
  • Settlement primitives, the in-process PTY backend, stream helpers, and fixture-payload validation are available for advanced testing infrastructure.