@bunli/test 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -0
- package/dist/helpers.d.ts +53 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1169 -0
- package/dist/index.js.map +13 -0
- package/dist/matchers.d.ts +38 -0
- package/dist/test-command.d.ts +5 -0
- package/dist/types.d.ts +67 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# @bunli/test
|
|
2
|
+
|
|
3
|
+
Testing utilities for Bunli CLI applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add -d @bunli/test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- ๐งช Test individual commands or entire CLIs
|
|
14
|
+
- ๐ญ Mock user prompts and shell commands
|
|
15
|
+
- โ
Built-in test matchers for CLI output
|
|
16
|
+
- ๐ Support for validation and retry scenarios
|
|
17
|
+
- ๐ TypeScript support with full type inference
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Basic Command Testing
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { test, expect } from 'bun:test'
|
|
25
|
+
import { defineCommand } from '@bunli/core'
|
|
26
|
+
import { testCommand, expectCommand } from '@bunli/test'
|
|
27
|
+
|
|
28
|
+
const greetCommand = defineCommand({
|
|
29
|
+
name: 'greet',
|
|
30
|
+
description: 'Greet someone',
|
|
31
|
+
handler: async ({ colors }) => {
|
|
32
|
+
console.log(colors.green('Hello, world!'))
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('greet command', async () => {
|
|
37
|
+
const result = await testCommand(greetCommand)
|
|
38
|
+
|
|
39
|
+
expectCommand(result).toHaveSucceeded()
|
|
40
|
+
expectCommand(result).toContainInStdout('[green]Hello, world![/green]')
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Testing with Flags
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const deployCommand = defineCommand({
|
|
48
|
+
name: 'deploy',
|
|
49
|
+
options: {
|
|
50
|
+
env: option(z.enum(['dev', 'prod'])),
|
|
51
|
+
force: option(z.boolean().default(false))
|
|
52
|
+
},
|
|
53
|
+
handler: async ({ flags }) => {
|
|
54
|
+
console.log(`Deploying to ${flags.env}${flags.force ? ' (forced)' : ''}`)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('deploy with flags', async () => {
|
|
59
|
+
const result = await testCommand(deployCommand, {
|
|
60
|
+
flags: { env: 'prod', force: true }
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(result.stdout).toContain('Deploying to prod (forced)')
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Mocking User Prompts
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { mockPromptResponses } from '@bunli/test'
|
|
71
|
+
|
|
72
|
+
const setupCommand = defineCommand({
|
|
73
|
+
name: 'setup',
|
|
74
|
+
handler: async ({ prompt }) => {
|
|
75
|
+
const name = await prompt('Project name:')
|
|
76
|
+
const useTs = await prompt.confirm('Use TypeScript?')
|
|
77
|
+
console.log(`Creating ${name} with${useTs ? '' : 'out'} TypeScript`)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('interactive setup', async () => {
|
|
82
|
+
const result = await testCommand(setupCommand, mockPromptResponses({
|
|
83
|
+
'Project name:': 'my-app',
|
|
84
|
+
'Use TypeScript?': 'y'
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
expect(result.stdout).toContain('Creating my-app with TypeScript')
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Mocking Shell Commands
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { mockShellCommands } from '@bunli/test'
|
|
95
|
+
|
|
96
|
+
const statusCommand = defineCommand({
|
|
97
|
+
name: 'status',
|
|
98
|
+
handler: async ({ shell }) => {
|
|
99
|
+
const branch = await shell`git branch --show-current`.text()
|
|
100
|
+
const status = await shell`git status --porcelain`.text()
|
|
101
|
+
console.log(`On branch: ${branch.trim()}`)
|
|
102
|
+
console.log(`Clean: ${status.trim() === ''}`)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('git status', async () => {
|
|
107
|
+
const result = await testCommand(statusCommand, mockShellCommands({
|
|
108
|
+
'git branch --show-current': 'feature/awesome\n',
|
|
109
|
+
'git status --porcelain': ''
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
expect(result.stdout).toContain('On branch: feature/awesome')
|
|
113
|
+
expect(result.stdout).toContain('Clean: true')
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Testing Validation with Retries
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const emailCommand = defineCommand({
|
|
121
|
+
name: 'register',
|
|
122
|
+
handler: async ({ prompt }) => {
|
|
123
|
+
const email = await prompt('Enter email:', {
|
|
124
|
+
schema: z.string().email()
|
|
125
|
+
})
|
|
126
|
+
console.log(`Registered: ${email}`)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('email validation', async () => {
|
|
131
|
+
const result = await testCommand(emailCommand, mockPromptResponses({
|
|
132
|
+
'Enter email:': ['invalid', 'still-bad', 'valid@email.com']
|
|
133
|
+
}))
|
|
134
|
+
|
|
135
|
+
// First two attempts fail validation
|
|
136
|
+
expect(result.stderr).toContain('Invalid email')
|
|
137
|
+
// Third attempt succeeds
|
|
138
|
+
expect(result.stdout).toContain('Registered: valid@email.com')
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Testing Complete CLIs
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { createCLI } from '@bunli/core'
|
|
146
|
+
import { testCLI } from '@bunli/test'
|
|
147
|
+
|
|
148
|
+
test('CLI help', async () => {
|
|
149
|
+
const result = await testCLI(
|
|
150
|
+
(cli) => {
|
|
151
|
+
cli.command('hello', {
|
|
152
|
+
description: 'Say hello',
|
|
153
|
+
handler: async () => console.log('Hello!')
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
['--help']
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
expectCommand(result).toContainInStdout('Say hello')
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Using Helper Functions
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { mockInteractive, mergeTestOptions } from '@bunli/test'
|
|
167
|
+
|
|
168
|
+
test('complex interaction', async () => {
|
|
169
|
+
const result = await testCommand(myCommand, mockInteractive(
|
|
170
|
+
{
|
|
171
|
+
'Name:': 'Alice',
|
|
172
|
+
'Continue?': 'y'
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
'npm --version': '10.0.0\n'
|
|
176
|
+
}
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
// Or merge multiple option sets
|
|
180
|
+
const result2 = await testCommand(myCommand, mergeTestOptions(
|
|
181
|
+
{ flags: { verbose: true } },
|
|
182
|
+
mockPromptResponses({ 'Name:': 'Bob' }),
|
|
183
|
+
{ env: { NODE_ENV: 'test' } }
|
|
184
|
+
))
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Test Matchers
|
|
189
|
+
|
|
190
|
+
The `expectCommand` function provides CLI-specific test matchers:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Exit code assertions
|
|
194
|
+
expectCommand(result).toHaveExitCode(0)
|
|
195
|
+
expectCommand(result).toHaveSucceeded() // exit code 0
|
|
196
|
+
expectCommand(result).toHaveFailed() // exit code !== 0
|
|
197
|
+
|
|
198
|
+
// Output assertions
|
|
199
|
+
expectCommand(result).toContainInStdout('success')
|
|
200
|
+
expectCommand(result).toContainInStderr('error')
|
|
201
|
+
expectCommand(result).toMatchStdout(/pattern/)
|
|
202
|
+
expectCommand(result).toMatchStderr(/error.*occurred/)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### `testCommand(command, options?)`
|
|
208
|
+
|
|
209
|
+
Test a single command.
|
|
210
|
+
|
|
211
|
+
**Parameters:**
|
|
212
|
+
- `command`: Command to test
|
|
213
|
+
- `options`: Test options
|
|
214
|
+
- `flags`: Command flags
|
|
215
|
+
- `args`: Positional arguments
|
|
216
|
+
- `env`: Environment variables
|
|
217
|
+
- `cwd`: Working directory
|
|
218
|
+
- `stdin`: Input lines (string or array)
|
|
219
|
+
- `mockPrompts`: Map of prompt messages to responses
|
|
220
|
+
- `mockShellCommands`: Map of shell commands to outputs
|
|
221
|
+
- `exitCode`: Expected exit code
|
|
222
|
+
|
|
223
|
+
**Returns:** `TestResult` with stdout, stderr, exitCode, duration, and error
|
|
224
|
+
|
|
225
|
+
### `testCLI(setupFn, argv, options?)`
|
|
226
|
+
|
|
227
|
+
Test a complete CLI with multiple commands.
|
|
228
|
+
|
|
229
|
+
**Parameters:**
|
|
230
|
+
- `setupFn`: Function to configure the CLI
|
|
231
|
+
- `argv`: Command line arguments
|
|
232
|
+
- `options`: Test options (same as testCommand)
|
|
233
|
+
|
|
234
|
+
### Helper Functions
|
|
235
|
+
|
|
236
|
+
- `mockPromptResponses(responses)`: Create options with mock prompt responses
|
|
237
|
+
- `mockShellCommands(commands)`: Create options with mock shell outputs
|
|
238
|
+
- `mockInteractive(prompts, commands?)`: Combine prompt and shell mocks
|
|
239
|
+
- `mockValidationAttempts(attempts)`: Create stdin for validation testing
|
|
240
|
+
- `mergeTestOptions(...options)`: Merge multiple test option objects
|
|
241
|
+
|
|
242
|
+
## Tips
|
|
243
|
+
|
|
244
|
+
1. **Colors in Output**: The test utilities preserve color codes as tags (e.g., `[green]text[/green]`) for easier assertion
|
|
245
|
+
|
|
246
|
+
2. **Multiple Attempts**: For validation scenarios, provide arrays of responses:
|
|
247
|
+
```typescript
|
|
248
|
+
mockPromptResponses({
|
|
249
|
+
'Enter age:': ['abc', '-5', '25'] // Tries each until valid
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
3. **Default Mocks**: Common commands have default mock responses:
|
|
254
|
+
- `git branch --show-current`: Returns `main\n`
|
|
255
|
+
- `git status`: Returns `nothing to commit, working tree clean\n`
|
|
256
|
+
|
|
257
|
+
4. **Schema Validation**: The mock prompt automatically handles Standard Schema validation and retry logic
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TestOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Helper to create test options with mock prompt responses
|
|
4
|
+
* @param responses - Map of prompt messages to responses
|
|
5
|
+
* @example
|
|
6
|
+
* mockPromptResponses({
|
|
7
|
+
* 'Enter name:': 'Alice',
|
|
8
|
+
* 'Enter age:': ['invalid', '25'], // Multiple attempts for validation
|
|
9
|
+
* 'Continue?': 'y'
|
|
10
|
+
* })
|
|
11
|
+
*/
|
|
12
|
+
export declare function mockPromptResponses(responses: Record<string, string | string[]>): Pick<TestOptions, 'mockPrompts'>;
|
|
13
|
+
/**
|
|
14
|
+
* Helper to create test options with mock shell command outputs
|
|
15
|
+
* @param commands - Map of shell commands to their outputs
|
|
16
|
+
* @example
|
|
17
|
+
* mockShellCommands({
|
|
18
|
+
* 'git status': 'On branch main\nnothing to commit',
|
|
19
|
+
* 'npm --version': '10.2.0',
|
|
20
|
+
* 'node --version': 'v20.10.0'
|
|
21
|
+
* })
|
|
22
|
+
*/
|
|
23
|
+
export declare function mockShellCommands(commands: Record<string, string>): Pick<TestOptions, 'mockShellCommands'>;
|
|
24
|
+
/**
|
|
25
|
+
* Helper to create test options for interactive commands
|
|
26
|
+
* @param prompts - Prompt responses
|
|
27
|
+
* @param commands - Shell command outputs
|
|
28
|
+
* @example
|
|
29
|
+
* mockInteractive(
|
|
30
|
+
* { 'Name:': 'Alice', 'Continue?': 'y' },
|
|
31
|
+
* { 'git status': 'clean' }
|
|
32
|
+
* )
|
|
33
|
+
*/
|
|
34
|
+
export declare function mockInteractive(prompts: Record<string, string | string[]>, commands?: Record<string, string>): TestOptions;
|
|
35
|
+
/**
|
|
36
|
+
* Helper to create stdin input for validation testing
|
|
37
|
+
* Useful for testing retry behavior with invalid inputs
|
|
38
|
+
* @param attempts - Array of input attempts
|
|
39
|
+
* @example
|
|
40
|
+
* mockValidationAttempts(['invalid-email', 'still-bad', 'valid@email.com'])
|
|
41
|
+
*/
|
|
42
|
+
export declare function mockValidationAttempts(attempts: string[]): Pick<TestOptions, 'stdin'>;
|
|
43
|
+
/**
|
|
44
|
+
* Helper to combine multiple test option objects
|
|
45
|
+
* @param options - Test option objects to merge
|
|
46
|
+
* @example
|
|
47
|
+
* mergeTestOptions(
|
|
48
|
+
* { flags: { verbose: true } },
|
|
49
|
+
* mockPromptResponses({ 'Name:': 'Alice' }),
|
|
50
|
+
* { env: { NODE_ENV: 'test' } }
|
|
51
|
+
* )
|
|
52
|
+
*/
|
|
53
|
+
export declare function mergeTestOptions(...options: Partial<TestOptions>[]): TestOptions;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { testCommand, testCLI } from './test-command.js';
|
|
2
|
+
export { expectCommand, createMatchers } from './matchers.js';
|
|
3
|
+
export { mockPromptResponses, mockShellCommands, mockInteractive, mockValidationAttempts, mergeTestOptions } from './helpers.js';
|
|
4
|
+
export type { TestOptions, TestResult, MockHandlerArgs, MockShell, ShellPromise, Matchers } from './types.js';
|