citty-test-utils
v1.0.2
Published
Unified testing framework for CLI applications with auto-detecting local/cleanroom execution, vitest config integration, and simplified scenario DSL.
Downloads
164
Maintainers
Readme
citty-test-utils
v1.0.0 - Unified testing framework for CLI applications with auto-detecting execution modes and vitest config integration
✨ Features
- 🎯 Unified API - Single
runCitty()function for local and cleanroom modes - ⚙️ Config-First - Configure once in
vitest.config.js, use everywhere - 🔄 Auto-Detection - Automatically selects local vs Docker based on config
- 📝 Simplified DSL -
.step(name, args)- no more.run()calls - 🐳 Auto Cleanroom - Automatic Docker setup/teardown (no manual lifecycle)
- 🧪 Fluent Assertions - Chainable test assertions
- 📸 Snapshot Testing - Built-in snapshot support
- 🔍 CLI Coverage - Analyze command coverage
📦 Installation
npm install --save-dev citty-test-utils vitestRequirements:
- Node.js 18+
- Vitest 1.0+
- Docker (optional, for cleanroom mode)
🚀 Quick Start
1. Configure vitest.config.js
// vitest.config.js
export default {
test: {
citty: {
cliPath: './src/cli.mjs', // Your CLI entry point
cleanroom: {
enabled: false // Set true for Docker isolation
}
}
}
}2. Write Tests
import { describe, it } from 'vitest'
import { runCitty, scenario } from 'citty-test-utils'
describe('CLI Tests', () => {
it('shows help', async () => {
// Config comes from vitest.config.js automatically!
const result = await runCitty(['--help'])
result
.expectSuccess()
.expectOutput(/USAGE/)
})
it('multi-step workflow', async () => {
await scenario('Build process')
.step('Check version', '--version').expectSuccess()
.step('Build prod', ['build', '--prod']).expectSuccess()
.execute() // Auto-detects local vs cleanroom
})
})3. Run Tests
npm test🎯 Core API
runCitty(args, options?)
Unified runner that auto-detects local vs cleanroom mode.
import { runCitty } from 'citty-test-utils'
// Basic usage (uses vitest.config.js settings)
const result = await runCitty(['--help'])
// Override config
const result = await runCitty(['test'], {
cliPath: './custom-cli.js',
timeout: 60000,
env: { NODE_ENV: 'production' }
})
// Force cleanroom mode
const result = await runCitty(['build'], {
cleanroom: { enabled: true }
})Options:
cliPath- Path to CLI entry point (defaults from config)cwd- Working directory (defaults to process.cwd())env- Environment variablestimeout- Execution timeout in mscleanroom.enabled- Force cleanroom modecleanroom.image- Docker image (default: 'node:20-alpine')
Config Hierarchy:
optionsparameter (highest priority)vitest.config.jstest.citty section- Environment variables
- Smart defaults
scenario(name)
Simplified scenario DSL for multi-step tests.
import { scenario } from 'citty-test-utils'
await scenario('User workflow')
// v1.0.0 API: args combined with step
.step('Show help', '--help')
.expectSuccess()
.expectOutput(/USAGE/)
.step('Build project', ['build', '--prod'])
.expectSuccess()
.expectOutput('Build complete')
.step('Run tests', ['test', '--coverage'])
.expectSuccess()
.execute() // Auto-detects mode from configMethods:
.step(name, args, options?)- Define test step with args.expectSuccess()- Assert exit code 0.expectFailure()- Assert non-zero exit code.expectExit(code)- Assert specific exit code.expectOutput(pattern)- Assert stdout matches pattern.expectError(pattern)- Assert stderr matches pattern.concurrent()- Run steps in parallel.execute()- Run scenario (auto-detects mode)
Fluent Assertions
All results include chainable assertions:
result
.expectSuccess() // Exit code 0
.expectFailure() // Exit code != 0
.expectExit(1) // Specific exit code
.expectOutput(/pattern/) // Stdout matches
.expectStderr(/error/) // Stderr matches
.expectOutputLength(10) // Min output length📖 Usage Examples
Example 1: Basic CLI Testing
import { describe, it } from 'vitest'
import { runCitty } from 'citty-test-utils'
describe('CLI Basic Tests', () => {
it('displays version', async () => {
const result = await runCitty(['--version'])
result
.expectSuccess()
.expectOutput(/\d+\.\d+\.\d+/)
})
it('shows help text', async () => {
const result = await runCitty(['--help'])
result
.expectSuccess()
.expectOutput(/USAGE/)
.expectOutput(/OPTIONS/)
})
})Example 2: Multi-Step Scenarios
import { scenario } from 'citty-test-utils'
describe('Project Workflow', () => {
it('initializes and builds project', async () => {
await scenario('Full workflow')
.step('Initialize', ['init', 'my-project'])
.expectSuccess()
.expectOutput('Created my-project')
.step('Install deps', ['install'], { cwd: './my-project' })
.expectSuccess()
.step('Build', ['build'], { cwd: './my-project' })
.expectSuccess()
.expectOutput('Build complete')
.execute()
})
})Example 3: Cleanroom (Docker) Testing
describe('Isolated Tests', () => {
it('runs in Docker container', async () => {
// Override config to force cleanroom
const result = await runCitty(['build', '--prod'], {
cleanroom: {
enabled: true,
image: 'node:20-alpine',
timeout: 60000
}
})
result.expectSuccess()
})
})Or configure globally in vitest.config.js:
export default {
test: {
citty: {
cliPath: './src/cli.mjs',
cleanroom: {
enabled: true // All tests use Docker
}
}
}
}Example 4: Snapshot Testing
import { runCitty, matchSnapshot } from 'citty-test-utils'
describe('Output Snapshots', () => {
it('matches help output snapshot', async () => {
const result = await runCitty(['--help'])
result.expectSuccess()
matchSnapshot(result.stdout, 'help-output')
})
})⚙️ Configuration
vitest.config.js
export default {
test: {
// citty-test-utils configuration
citty: {
// Required: CLI entry point
cliPath: './src/cli.mjs',
// Optional: working directory
cwd: process.cwd(),
// Optional: cleanroom settings
cleanroom: {
enabled: false, // Enable Docker isolation
image: 'node:20-alpine', // Docker image
timeout: 30000, // Timeout in ms
env: { // Environment variables
NODE_ENV: 'test'
}
}
},
// Standard vitest config
globals: true,
testTimeout: 30000
}
}Environment Variables
# Override cliPath
export TEST_CLI_PATH=./custom-cli.js
# Override working directory
export TEST_CWD=/path/to/projectConfig Priority
- Function
optionsparameter (highest) vitest.config.jstest.citty section- Environment variables
- Defaults (lowest)
🔧 Advanced Features
Auto Cleanroom Lifecycle
No manual setup/teardown needed! Just enable in config:
// vitest.config.js
export default {
test: {
citty: {
cleanroom: { enabled: true }
}
}
}Tests automatically run in Docker with cleanup:
// No beforeAll/afterAll needed!
it('runs isolated', async () => {
const result = await runCitty(['test'])
result.expectSuccess()
})Concurrent Scenarios
await scenario('Parallel tests')
.concurrent() // Enable parallel execution
.step('Test 1', 'test-1')
.step('Test 2', 'test-2')
.step('Test 3', 'test-3')
.execute()Custom Assertions
result.expect(result => {
if (!result.stdout.includes('custom')) {
throw new Error('Custom check failed')
}
})📊 What's New in v1.0.0
Breaking Changes ⚠️
- Unified API:
runCitty()replacesrunLocalCitty()and cleanroom setup - Scenario DSL:
.step(name, args)instead of.step(name).run(args) - Config-First: Configure in
vitest.config.jsinstead of every test - Auto Cleanroom: No manual setup/teardown needed
Migration from v0.6.x
Old API (v0.6.x):
import { runLocalCitty, setupCleanroom } from 'citty-test-utils'
await runLocalCitty({
args: ['--help'],
cliPath: './src/cli.mjs',
cwd: process.cwd()
})New API (v1.0.0):
import { runCitty } from 'citty-test-utils'
// Config from vitest.config.js
await runCitty(['--help'])See Migration Guide for complete details.
🐛 Troubleshooting
Error: "CLI path not configured"
Solution: Add cliPath to vitest.config.js:
export default {
test: {
citty: {
cliPath: './src/cli.mjs'
}
}
}Error: "Cannot find module"
Solution: Check cliPath is correct:
const result = await runCitty(['--help'], {
cliPath: './src/cli.mjs' // Absolute or relative to cwd
})Cleanroom not working
Solution: Enable in config:
export default {
test: {
citty: {
cleanroom: { enabled: true }
}
}
}Or per-test:
const result = await runCitty(['test'], {
cleanroom: { enabled: true }
})📚 Documentation
🤝 Contributing
# Clone repo
git clone https://github.com/seanchatmangpt/citty-test-utils
cd citty-test-utils
# Install deps
npm install
# Run tests
npm test
# Run specific tests
npm run test:unit
npm run test:integration📝 License
MIT © GitVan Team
🙏 Acknowledgments
Built with:
- Citty - CLI framework
- Vitest - Test framework
- Testcontainers - Docker integration
- Zod - Schema validation
Need help? Open an issue
Love citty-test-utils? Give us a ⭐ on GitHub!
