@inquirer/testing
Version:
Inquirer testing utilities
227 lines (160 loc) • 7.45 kB
Markdown
# `@inquirer/testing`
The `@inquirer/testing` package is Inquirer's answer to testing prompts [built with `@inquirer/core`](https://github.com/SBoudrias/Inquirer.js/tree/main/packages/core).
# Installation
<table>
<tr>
<th>npm</th>
<th>yarn</th>
</tr>
<tr>
<td>
```sh
npm install @inquirer/testing --save-dev
```
</td>
<td>
```sh
yarn add @inquirer/testing --dev
```
</td>
</tr>
</table>
# Usage
This package provides two ways to test Inquirer prompts:
1. **Unit testing** with `render()` - Test individual prompts in isolation
2. **E2E testing** with `screen` - Test full CLI applications that use Inquirer
## Unit Testing with `render()`
The `render()` function creates and instruments a command line interface for testing a single prompt.
```ts
import { render } from '@inquirer/testing';
import input from '@inquirer/input';
describe('input prompt', () => {
it('handle simple use case', async () => {
const { answer, events, getScreen } = await render(input, {
message: 'What is your name',
});
expect(getScreen()).toMatchInlineSnapshot(`"? What is your name"`);
events.type('J');
expect(getScreen()).toMatchInlineSnapshot(`"? What is your name J"`);
events.type('ohn');
events.keypress('enter');
await expect(answer).resolves.toEqual('John');
expect(getScreen()).toMatchInlineSnapshot(`"? What is your name John"`);
});
});
```
### `render()` API
`render` takes 2 arguments:
1. The Inquirer prompt to test (the return value of `createPrompt()`)
2. The prompt configuration (the first prompt argument)
`render` returns a promise that resolves once the prompt is rendered. This promise returns:
- `answer` (`Promise`) - Resolves when an answer is provided and valid
- `getScreen` (`({ raw?: boolean }) => string`) - Returns the current screen content. By default strips ANSI codes
- `nextRender` (`() => Promise<void>`) - Wait for the next screen update. Use after triggering async actions (e.g. pressing enter with validation). Coalesces rapid back-to-back renders so a single `await nextRender()` captures the final settled state
- `events` - Utilities to interact with the prompt:
- `keypress(key: string | KeyObject)` - Trigger a keypress event
- `type(text: string)` - Type text into the prompt
- `getFullOutput` (`() => Promise<string>`) - Returns the full output interpreted through a virtual terminal, resolving ANSI escape sequences into the actual screen state
### Async actions and `nextRender()`
When a keypress triggers an asynchronous action (such as input validation), the screen won't update synchronously. Use `nextRender()` to wait for the prompt to settle before reading the screen:
```ts
import { render } from '@inquirer/testing';
import input from '@inquirer/input';
it('shows a validation error', async () => {
const { answer, events, getScreen, nextRender } = await render(input, {
message: 'Enter a number',
validate: (value) => /^\d+$/.test(value) || 'Must be a number',
});
events.type('abc');
events.keypress('enter');
await nextRender(); // wait for validation to complete and the error to render
expect(getScreen()).toContain('Must be a number');
events.keypress('backspace');
events.keypress('backspace');
events.keypress('backspace');
events.type('42');
events.keypress('enter');
await expect(answer).resolves.toEqual('42');
});
```
### Unit Testing Example
You can refer to the [`@inquirer/input` test suite](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/input/input.test.ts) for a comprehensive unit testing example using `render()`.
## E2E Testing with `screen`
For testing full CLI applications that use Inquirer prompts internally, use the framework-specific entry points:
### Vitest
```ts
import { describe, it, expect } from 'vitest';
import { screen } from '@inquirer/testing/vitest';
// Import your CLI AFTER @inquirer/testing/vitest
import { runMyCli } from './my-cli.js';
describe('my CLI', () => {
it('asks for name and confirms', async () => {
const result = runMyCli();
// First prompt is immediately available
expect(screen.getScreen()).toContain('What is your name?');
screen.type('John');
screen.keypress('enter');
// Wait for next prompt
await screen.next();
expect(screen.getScreen()).toContain('Confirm?');
screen.keypress('enter');
await result;
});
});
```
### Jest
```ts
import { screen } from '@inquirer/testing/jest';
import { runMyCli } from './my-cli.js';
describe('my CLI', () => {
it('asks for name and confirms', async () => {
const result = runMyCli();
// First prompt is immediately available
expect(screen.getScreen()).toContain('What is your name?');
screen.type('John');
screen.keypress('enter');
// Wait for next prompt
await screen.next();
expect(screen.getScreen()).toContain('Confirm?');
screen.keypress('enter');
await result;
});
});
```
### `screen` API
The `screen` object provides:
- `next()` - Wait for the next screen update (prompt transitions, validation errors, async updates). The initial prompt render is available immediately via `getScreen()` — no `next()` needed
- `getScreen({ raw?: boolean })` - Get the current prompt screen content. By default strips ANSI codes
- `getFullOutput({ raw?: boolean })` - Get all accumulated output interpreted through a virtual terminal (returns a `Promise`). By default resolves ANSI escape sequences into actual screen state
- `type(text)` - Type text (writes to stream AND emits keypresses)
- `keypress(key)` - Send a keypress event
- `clear()` - Reset screen state (called automatically before each test)
### Mocking Third-Party Prompts
All `@inquirer/*` prompts are mocked automatically. To mock a third-party or custom prompt package, use `wrapPrompt` in your own mock call:
#### Vitest
```ts
import { screen, wrapPrompt } from '@inquirer/testing/vitest';
vi.mock('@my-company/custom-prompt', async (importOriginal) => {
const actual = await importOriginal<typeof import('@my-company/custom-prompt')>();
return { ...actual, default: wrapPrompt(actual.default) };
});
```
#### Jest
In Jest, `jest.mock()` factories are hoisted before imports, so `wrapPrompt` must be accessed via `jest.requireActual()` inside the factory:
```ts
import { screen } from '@inquirer/testing/jest';
jest.mock('@my-company/custom-prompt', () => {
const { wrapPrompt } = jest.requireActual('@inquirer/testing/jest');
const actual = jest.requireActual('@my-company/custom-prompt');
return { ...actual, default: wrapPrompt(actual.default) };
});
```
### Important Notes
1. **Import order matters**: Import `@inquirer/testing/vitest` or `@inquirer/testing/jest` BEFORE importing modules that use Inquirer prompts
2. **Editor prompt**: The external editor is mocked — `screen.type()` buffers text, and `screen.keypress('enter')` submits it (same pattern as other prompts). Works with both `waitForUserInput: true` and `false`
3. **Sequential prompts**: Multiple prompts are supported, but they must run sequentially (not concurrently)
### E2E Testing Example
You can refer to the [`@inquirer/demo` test suite](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/demo/demo.test.ts) for a comprehensive E2E testing example using `screen`.
# License
Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))<br/>
Licensed under the MIT license.