jest-runner-cli
v0.2.8
Published
Jest custom runner for CLI workflows with a minimal CliRunner helper
Readme
jest-runner-cli
A custom Jest runner with an imperative CLI process helper for testing CLI applications.
jest-runner-cli is a lightweight Jest runner package that provides:
- A custom Jest runner built with
create-jest-runnerfor seamless integration with Jest 29.6.1+ - A
CliRunnerhelper class to spawn and interact with child processes in tests - Full support for stdout/stderr monitoring, JSON parsing, and process management
Perfect for testing CLI tools, scripts, and command-line applications in your Jest test suite.
Features
✅ Custom Jest Runner — Drop-in replacement for running tests with a custom runner
✅ CliRunner Helper — Easy-to-use imperative API for spawning and controlling CLI processes
✅ Flexible Output Reading — Read stdout as lines, raw text, or parse JSON
✅ Auto-Exit Protection — Automatically detect and terminate hung processes
✅ Cross-Platform — Works on Windows, macOS, and Linux
✅ Jest 29+ Compatible — Fully tested with Jest 29.6.1 through 29.7.0+
✅ TypeScript Ready — Full type definitions included
⚠️ Limitations — Advanced retry strategies and custom signal handling not yet implemented
Installation
npm install --save-dev jest-runner-cliPeer Dependency: Jest ^29.6.1
Quick Start
1. Configure Jest
Update jest.config.js:
// jest.config.js (ESM)
export default {
runner: 'jest-runner-cli',
testMatch: ['<rootDir>/test/**/*.test.ts']
};2. Use in Tests
import { CliRunner } from 'jest-runner-cli';
describe('CLI testing', () => {
it('runs node -v and captures output', async () => {
const cli = new CliRunner();
cli.start({ command: process.execPath, args: ['-v'] });
const lines = await cli.readStdout().toLines(2000);
expect(lines[0]).toMatch(/^v\d+\.\d+\.\d+/);
await cli.sendCtrlC();
cli.dispose();
});
});Usage Guide
Jest Runner Configuration
The package acts as a Jest custom runner. Once configured in jest.config.js, Jest will automatically use it to execute your test files.
CliRunner API
Basic Usage
import { CliRunner } from 'jest-runner-cli';
const runner = new CliRunner();
// Start a process
runner.start({
command: 'node',
args: ['./my-script.js'],
cwd: process.cwd(),
env: process.env
});
// Write to stdin
runner.writeln('input data');
// Read output
const output = await runner.readStdout().toLines();
// Gracefully stop
await runner.sendCtrlC();
runner.dispose();Reading Output
// Read as array of lines
const lines = await runner.readStdout().toLines(2000); // timeout in ms
// Read as raw string
const text = await runner.readStdout(2000);
// Extract JSON
const json = await runner.readStdout().toJson(2000);
// Get stderr
const errors = runner.readStderr();
// Clear buffer
runner.readStdout().clear();Handling Process Events
// Listen for process exit
runner.on('exit', ({ code, signal }) => {
console.log(`Process exited with code ${code}`);
});
// Auto-exit on timeout (e.g., hung process)
runner.start({ command: 'node', args: ['long-running.js'] }, 5000); // 5s timeout
// Listen for auto-exit error
runner.once('error', (err) => {
if (err.message === 'auto-exit timeout reached') {
console.log('Process was auto-terminated');
}
});Complete Example
import { CliRunner } from 'jest-runner-cli';
describe('My CLI App', () => {
let cli: CliRunner;
beforeEach(() => {
cli = new CliRunner();
});
afterEach(async () => {
await cli.sendCtrlC().catch(() => {});
cli.dispose();
});
it('displays help text', async () => {
cli.start({ command: 'node', args: ['./bin/cli.js', '--help'] });
const output = await cli.readStdout().toLines(2000);
expect(output.join('\n')).toContain('Usage:');
});
it('handles JSON output', async () => {
cli.start({ command: 'node', args: ['./bin/cli.js', '--json'] });
const data = await cli.readStdout().toJson(2000);
expect(data).toHaveProperty('version');
});
it('detects hung process', async () => {
const error = await new Promise((resolve) => {
cli.once('error', resolve);
cli.start(
{ command: 'node', args: ['-e', 'setTimeout(() => {}, 60000)'] },
3000 // 3s timeout
);
});
expect(error.message).toBe('auto-exit timeout reached');
});
});API Reference
CliRunner
Methods
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| start() | SpawnOptions, exitWaitTimeout? | this | Spawn a child process. If exitWaitTimeout is set (ms), the process will auto-terminate if it doesn't exit within that time. |
| write() | data: string | void | Write to stdin without a newline. |
| writeln() | data: string | void | Write to stdin with a newline appended. |
| readStdout() | timeout?: number | Promise<string> | OutputHelper | Read stdout buffer. With timeout arg, returns raw string. Without arg, returns helper with .toLines(), .toJson(), .clear() methods. |
| readStderr() | — | string | Get stderr buffer (non-blocking). |
| sendCtrlC() | timeout?: number | Promise<void> | Send SIGINT and wait for process exit. Falls back to SIGKILL on timeout. |
| dispose() | — | void | Force-kill process and release resources. |
Events
| Event | Callback Arguments | Description |
|-------|-------------------|-------------|
| exit | { code, signal } | Process exited. |
| stdout | chunk: string | Data received on stdout. |
| stderr | chunk: string | Data received on stderr. |
| error | err: Error | Error occurred (e.g., auto-exit timeout). |
Types
type SpawnOptions = {
command?: string; // Required: command to execute
args?: string[]; // Command arguments
cwd?: string; // Working directory
env?: NodeJS.ProcessEnv; // Environment variables
};Project Structure
jest-runner-cli/
├── src/
│ ├── index.ts # Main entry point, Jest runner export
│ ├── run.ts # Jest runner implementation
│ └── CliRunner.ts # CliRunner class
├── test/unit/
│ └── cliRunner.test.ts # Unit tests
├── dist/ # Compiled output (generated)
├── jest.config.js # Jest configuration
├── tsconfig.json # TypeScript base config
├── tsconfig.build.json # TypeScript build config
└── package.json # Package metadataDevelopment
Setup
git clone https://github.com/yourusername/jest-runner-cli.git
cd jest-runner-cli
npm installCommon Commands
npm run build # Compile TypeScript
npm run test # Run tests
npm run lint # Check code quality
npm run type-check # Check TypeScript types
npm run docs # Generate TypeDoc documentation
npm run depcruise # Analyze dependenciesTesting
# Run all tests
npm test
# Run specific test file
npm test -- cliRunner.test.ts
# Run with coverage
npm run test:ciTechnical Details
- Runtime: Node.js 18+, TypeScript 5.3+
- Jest Version: 29.6.1+ (fully compatible including 29.7.0+)
- Module Format: CommonJS (compatible with ESM via package.json exports)
- Build: TypeScript compiled to
dist/folder with webpack bundling
Implementation Notes
Jest Runner Architecture: The package exports a Jest custom runner built on
create-jest-runner. Therun.tsfile implements thecreate-jest-runnerrun file API, which receives options{ testPath, globalConfig, config, ... }and delegates test execution tojest-circus/runner— Jest's standard test runner as of version 29+.jest-circus Integration: Starting from Jest 29.6.1+, we use
jest-circus/runneras the underlying test executor. This avoids direct references tojest-runner's non-public export paths, ensuring compatibility with Node's packageexportsconstraints.TypeScript Compilation: TypeScript is compiled with separate configs:
tsconfig.json— development (no emit, strict mode enabled)tsconfig.build.json— build (emits todist/, includes type definitions)- Webpack bundling produces the final output as CommonJS for broad compatibility
CliRunner Implementation: Based on Node.js
child_process.spawn()with event-driven stdout/stderr buffering. Auto-exit timeout usessetTimeoutto detect hung processes and escalates fromSIGINTtoSIGKILL.
Compatibility with Jest 29+
As of version 0.2.6+, jest-runner-cli is fully compatible with Jest 29.6.1 and later versions, including Jest 29.7.0+. The implementation correctly uses jest-circus/runner instead of direct references to jest-runner's private API paths, ensuring it works with Node's strict exports constraints.
Issue Fix (v0.2.6+): Earlier versions referenced jest-runner/build/runTest.js, which triggered ERR_PACKAGE_PATH_NOT_EXPORTED in Jest 29+ due to Node's package export restrictions. This has been resolved by delegating to the public Jest test runner interface through jest-circus/runner.
Troubleshooting
Process Not Starting
Error: No command provided
// ❌ Wrong
runner.start({});
// ✅ Correct
runner.start({ command: 'node', args: ['script.js'] });Timeout Reading Output
Error: stdout timeout
Increase the timeout value:
// Default 2000ms, increase if needed
const output = await runner.readStdout().toLines(5000);Process Still Running After sendCtrlC
On Windows, the process may not respond to SIGINT. The runner will auto-escalate to force-kill after timeout:
// Will escalate to taskkill after 2000ms
await runner.sendCtrlC();
// Or specify custom timeout
await runner.sendCtrlC(5000);Changelog
v0.2.6–0.2.7 (Latest)
- ✅ Fixed:
ERR_PACKAGE_PATH_NOT_EXPORTEDerror with Jest 29.6.1+- Changed from
jest-runnerdirect API tojest-circus/runnerfor test execution - Updated run.ts to use
create-jest-runnerrun file API ({ testPath, globalConfig, config }signature) - Ensured full compatibility with Node's strict package
exportsconstraints
- Changed from
- ✅ Updated integration tests with new run function signature
- ✅ Added repro test demonstrating Jest 29+ compatibility
v0.2.0
- ✅ Refactored to CommonJS module format for broad compatibility
- ✅ Integrated with
create-jest-runnerfor Jest custom runner functionality - ✅ Added comprehensive TypeScript type definitions
- ✅ Added auto-exit timeout feature for hung process detection
- ✅ Updated test suite with async/await patterns
v0.1.0
- Initial release with basic
CliRunnerfunctionality
License
MIT © 2026
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
For development, ensure:
- TypeScript strict mode is enabled
- All tests pass (
npm test) - Linting passes (
npm run lint) - New features include unit tests in
test/unit/
