@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 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;
@@ -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';