@reliverse/rempts-test 2.3.1

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,264 @@
1
+ # rempts-test
2
+
3
+ Testing utilities for Rempts CLI applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add -d rempts-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 '@reliverse/rempts-core'
26
+ import { testCommand, expectCommand } from '@reliverse/rempts-test'
27
+
28
+ const greetCommand = defineCommand({
29
+ name: 'greet',
30
+ description: 'Greet someone',
31
+ handler: async ({ colors }) => {
32
+ console.log(relico.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 '@reliverse/rempts-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 '@reliverse/rempts-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 '@reliverse/rempts-core'
146
+ import { testCLI } from '@reliverse/rempts-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 '@reliverse/rempts-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
+
213
+ - `command`: Command to test
214
+ - `options`: Test options
215
+ - `flags`: Command flags
216
+ - `args`: Positional arguments
217
+ - `env`: Environment variables
218
+ - `cwd`: Working directory
219
+ - `stdin`: Input lines (string or array)
220
+ - `mockPrompts`: Map of prompt messages to responses
221
+ - `mockShellCommands`: Map of shell commands to outputs
222
+ - `exitCode`: Expected exit code
223
+
224
+ **Returns:** `TestResult` with stdout, stderr, exitCode, duration, and error
225
+
226
+ ### `testCLI(setupFn, argv, options?)`
227
+
228
+ Test a complete CLI with multiple commands.
229
+
230
+ **Parameters:**
231
+
232
+ - `setupFn`: Function to configure the CLI
233
+ - `argv`: Command line arguments
234
+ - `options`: Test options (same as testCommand)
235
+
236
+ ### Helper Functions
237
+
238
+ - `mockPromptResponses(responses)`: Create options with mock prompt responses
239
+ - `mockShellCommands(commands)`: Create options with mock shell outputs
240
+ - `mockInteractive(prompts, commands?)`: Combine prompt and shell mocks
241
+ - `mockValidationAttempts(attempts)`: Create stdin for validation testing
242
+ - `mergeTestOptions(...options)`: Merge multiple test option objects
243
+
244
+ ## Tips
245
+
246
+ 1. **Colors in Output**: The test utilities preserve color codes as tags (e.g., `[green]text[/green]`) for easier assertion
247
+
248
+ 2. **Multiple Attempts**: For validation scenarios, provide arrays of responses:
249
+
250
+ ```typescript
251
+ mockPromptResponses({
252
+ 'Enter age:': ['abc', '-5', '25'] // Tries each until valid
253
+ })
254
+ ```
255
+
256
+ 3. **Default Mocks**: Common commands have default mock responses:
257
+ - `git branch --show-current`: Returns `main\n`
258
+ - `git status`: Returns `nothing to commit, working tree clean\n`
259
+
260
+ 4. **Schema Validation**: The mock prompt automatically handles Standard Schema validation and retry logic
261
+
262
+ ## License
263
+
264
+ 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,42 @@
1
+ export function mockPromptResponses(responses) {
2
+ return { mockPrompts: responses };
3
+ }
4
+ export function mockShellCommands(commands) {
5
+ return { mockShellCommands: commands };
6
+ }
7
+ export function mockInteractive(prompts, commands) {
8
+ return {
9
+ ...mockPromptResponses(prompts),
10
+ ...commands ? mockShellCommands(commands) : {}
11
+ };
12
+ }
13
+ export function mockValidationAttempts(attempts) {
14
+ return { stdin: attempts };
15
+ }
16
+ export function mergeTestOptions(...options) {
17
+ const merged = {};
18
+ for (const opt of options) {
19
+ if (opt.stdin || opt.mockPrompts) {
20
+ const stdinArray = [];
21
+ if (merged.stdin) {
22
+ if (Array.isArray(merged.stdin)) {
23
+ stdinArray.push(...merged.stdin);
24
+ } else {
25
+ stdinArray.push(merged.stdin);
26
+ }
27
+ }
28
+ if (opt.stdin) {
29
+ if (Array.isArray(opt.stdin)) {
30
+ stdinArray.push(...opt.stdin);
31
+ } else {
32
+ stdinArray.push(opt.stdin);
33
+ }
34
+ }
35
+ if (stdinArray.length > 0) {
36
+ merged.stdin = stdinArray;
37
+ }
38
+ }
39
+ Object.assign(merged, opt);
40
+ }
41
+ return merged;
42
+ }
@@ -0,0 +1,38 @@
1
+ import type { TestResult } from "./types.js";
2
+ export interface Matchers {
3
+ toHaveExitCode(code: number): void;
4
+ toHaveSucceeded(): void;
5
+ toHaveFailed(): void;
6
+ toContainInStdout(text: string): void;
7
+ toContainInStderr(text: string): void;
8
+ toMatchStdout(pattern: RegExp): void;
9
+ toMatchStderr(pattern: RegExp): void;
10
+ }
11
+ export declare function createMatchers(result: TestResult): Matchers;
12
+ declare global {
13
+ namespace jest {
14
+ interface Matchers<R> {
15
+ toHaveExitCode(code: number): R;
16
+ toHaveSucceeded(): R;
17
+ toHaveFailed(): R;
18
+ toContainInStdout(text: string): R;
19
+ toContainInStderr(text: string): R;
20
+ toMatchStdout(pattern: RegExp): R;
21
+ toMatchStderr(pattern: RegExp): R;
22
+ }
23
+ }
24
+ }
25
+ export declare function expectCommand(result: TestResult): {
26
+ toHaveExitCode(code: number): void;
27
+ toHaveSucceeded(): void;
28
+ toHaveFailed(): void;
29
+ toContainInStdout(text: string): void;
30
+ toContainInStderr(text: string): void;
31
+ toMatchStdout(pattern: RegExp): void;
32
+ toMatchStderr(pattern: RegExp): void;
33
+ stdout: string;
34
+ stderr: string;
35
+ exitCode: number;
36
+ duration: number;
37
+ error?: Error;
38
+ };
@@ -0,0 +1,62 @@
1
+ export function createMatchers(result) {
2
+ return {
3
+ toHaveExitCode(code) {
4
+ if (result.exitCode !== code) {
5
+ throw new Error(
6
+ `Expected exit code ${code}, but got ${result.exitCode}
7
+ stdout: ${result.stdout}
8
+ stderr: ${result.stderr}`
9
+ );
10
+ }
11
+ },
12
+ toHaveSucceeded() {
13
+ if (result.exitCode !== 0) {
14
+ throw new Error(
15
+ `Expected command to succeed (exit code 0), but got ${result.exitCode}
16
+ stdout: ${result.stdout}
17
+ stderr: ${result.stderr}
18
+ ` + (result.error ? `error: ${result.error.message}` : "")
19
+ );
20
+ }
21
+ },
22
+ toHaveFailed() {
23
+ if (result.exitCode === 0) {
24
+ throw new Error(
25
+ `Expected command to fail (non-zero exit code), but it succeeded
26
+ stdout: ${result.stdout}`
27
+ );
28
+ }
29
+ },
30
+ toContainInStdout(text) {
31
+ if (!result.stdout.includes(text)) {
32
+ throw new Error(`Expected stdout to contain "${text}"
33
+ stdout: ${result.stdout}`);
34
+ }
35
+ },
36
+ toContainInStderr(text) {
37
+ if (!result.stderr.includes(text)) {
38
+ throw new Error(`Expected stderr to contain "${text}"
39
+ stderr: ${result.stderr}`);
40
+ }
41
+ },
42
+ toMatchStdout(pattern) {
43
+ if (!pattern.test(result.stdout)) {
44
+ throw new Error(`Expected stdout to match ${pattern}
45
+ stdout: ${result.stdout}`);
46
+ }
47
+ },
48
+ toMatchStderr(pattern) {
49
+ if (!pattern.test(result.stderr)) {
50
+ throw new Error(`Expected stderr to match ${pattern}
51
+ stderr: ${result.stderr}`);
52
+ }
53
+ }
54
+ };
55
+ }
56
+ export function expectCommand(result) {
57
+ const matchers = createMatchers(result);
58
+ return {
59
+ ...result,
60
+ ...matchers
61
+ };
62
+ }
package/dist/mod.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { mergeTestOptions, mockInteractive, mockPromptResponses, mockShellCommands, mockValidationAttempts, } from "./helpers.js";
2
+ export { createMatchers, expectCommand } from "./matchers.js";
3
+ export { testCLI, testCommand } from "./test-command.js";
4
+ export type { Matchers, MockHandlerArgs, MockShell, ShellPromise, TestOptions, TestResult, } from "./types.js";
package/dist/mod.js ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ mergeTestOptions,
3
+ mockInteractive,
4
+ mockPromptResponses,
5
+ mockShellCommands,
6
+ mockValidationAttempts
7
+ } from "./helpers.js";
8
+ export { createMatchers, expectCommand } from "./matchers.js";
9
+ export { testCLI, testCommand } from "./test-command.js";
@@ -0,0 +1,4 @@
1
+ import type { CLI, Command } from "@reliverse/rempts-core";
2
+ import type { TestOptions, TestResult } from "./types.js";
3
+ export declare function testCommand(command: Command<any>, options?: TestOptions): Promise<TestResult>;
4
+ export declare function testCLI(setupCLI: (cli: CLI) => void, argv: string[], _options?: Omit<TestOptions, "args">): Promise<TestResult>;
@@ -0,0 +1,360 @@
1
+ import { createCLI } from "@reliverse/rempts-core";
2
+ export async function testCommand(command, options = {}) {
3
+ const startTime = performance.now();
4
+ const stdout = [];
5
+ const stderr = [];
6
+ let exitCode = 0;
7
+ let error;
8
+ const stdinLines = Array.isArray(options.stdin) ? [...options.stdin] : options.stdin ? [options.stdin] : [];
9
+ const mockPromptsMap = options.mockPrompts || {};
10
+ const promptResponsesUsed = /* @__PURE__ */ new Map();
11
+ const mockPrompt = Object.assign(
12
+ async (message, options2) => {
13
+ stdout.push(message);
14
+ let response;
15
+ if (mockPromptsMap[message]) {
16
+ const responses = mockPromptsMap[message];
17
+ const usedCount = promptResponsesUsed.get(message) || 0;
18
+ if (Array.isArray(responses)) {
19
+ response = responses[usedCount] ?? responses.at(-1) ?? "";
20
+ promptResponsesUsed.set(message, usedCount + 1);
21
+ } else {
22
+ response = responses ?? "";
23
+ }
24
+ } else {
25
+ response = stdinLines.shift() || "";
26
+ }
27
+ stdout.push(response);
28
+ if (options2?.schema) {
29
+ const result = await options2.schema["~standard"].validate(response);
30
+ if (result.issues) {
31
+ stderr.push("[red]Invalid input:[/red]");
32
+ for (const issue of result.issues) {
33
+ stderr.push(`[dim] \u2022 ${issue.message}[/dim]`);
34
+ }
35
+ const hasMoreMockResponses = mockPromptsMap[message] && Array.isArray(mockPromptsMap[message]) && (promptResponsesUsed.get(message) || 0) < mockPromptsMap[message].length;
36
+ const hasMoreStdin = stdinLines.length > 0;
37
+ if (hasMoreMockResponses || hasMoreStdin) {
38
+ return mockPrompt(message, options2);
39
+ }
40
+ return void 0;
41
+ }
42
+ return result.value;
43
+ }
44
+ if (options2?.validate) {
45
+ const validationResult = options2.validate(response);
46
+ if (validationResult !== true) {
47
+ const errorMsg = typeof validationResult === "string" ? validationResult : "Invalid input";
48
+ stderr.push(`\u2717 ${errorMsg}`);
49
+ if (stdinLines.length > 0) {
50
+ return mockPrompt(message, options2);
51
+ }
52
+ }
53
+ }
54
+ return response;
55
+ },
56
+ {
57
+ confirm: async (message, opts) => {
58
+ stdout.push(message);
59
+ let response;
60
+ if (mockPromptsMap[message]) {
61
+ const responses = mockPromptsMap[message];
62
+ const usedCount = promptResponsesUsed.get(message) || 0;
63
+ if (Array.isArray(responses)) {
64
+ response = responses[usedCount] ?? responses.at(-1) ?? "";
65
+ promptResponsesUsed.set(message, usedCount + 1);
66
+ } else {
67
+ response = responses ?? "";
68
+ }
69
+ } else {
70
+ response = stdinLines.shift() || "";
71
+ }
72
+ stdout.push(response);
73
+ const normalized = response.toLowerCase().trim();
74
+ return normalized === "y" || normalized === "yes" || opts?.default && normalized === "";
75
+ },
76
+ select: async (message, selectOptions) => {
77
+ stdout.push(message);
78
+ selectOptions.options.forEach((choice2, i) => {
79
+ const label = typeof choice2 === "object" ? choice2.label : choice2;
80
+ stdout.push(` ${i + 1}. ${label}`);
81
+ });
82
+ let response;
83
+ if (mockPromptsMap[message]) {
84
+ const responses = mockPromptsMap[message];
85
+ const usedCount = promptResponsesUsed.get(message) || 0;
86
+ if (Array.isArray(responses)) {
87
+ response = responses[usedCount] ?? responses.at(-1) ?? "";
88
+ promptResponsesUsed.set(message, usedCount + 1);
89
+ } else {
90
+ response = responses ?? "";
91
+ }
92
+ } else {
93
+ response = stdinLines.shift() || "1";
94
+ }
95
+ stdout.push(`> ${response}`);
96
+ const index = Number.parseInt(response, 10) - 1;
97
+ const choice = selectOptions.options[index] || selectOptions.options[0];
98
+ return typeof choice === "object" ? choice.value : choice;
99
+ },
100
+ password: async (message, options2) => {
101
+ stdout.push(message);
102
+ let response;
103
+ if (mockPromptsMap[message]) {
104
+ const responses = mockPromptsMap[message];
105
+ const usedCount = promptResponsesUsed.get(message) || 0;
106
+ if (Array.isArray(responses)) {
107
+ response = responses[usedCount] ?? responses.at(-1) ?? "";
108
+ promptResponsesUsed.set(message, usedCount + 1);
109
+ } else {
110
+ response = responses ?? "";
111
+ }
112
+ } else {
113
+ response = stdinLines.shift() || "";
114
+ }
115
+ stdout.push("*".repeat(response.length));
116
+ if (options2?.schema) {
117
+ const result = await options2.schema["~standard"].validate(response);
118
+ if (result.issues) {
119
+ stderr.push("[red]Invalid input:[/red]");
120
+ for (const issue of result.issues) {
121
+ stderr.push(`[dim] \u2022 ${issue.message}[/dim]`);
122
+ }
123
+ const hasMoreMockResponses = mockPromptsMap[message] && Array.isArray(mockPromptsMap[message]) && (promptResponsesUsed.get(message) || 0) < mockPromptsMap[message].length;
124
+ const hasMoreStdin = stdinLines.length > 0;
125
+ if (hasMoreMockResponses || hasMoreStdin) {
126
+ return mockPrompt.password(message, options2);
127
+ }
128
+ return void 0;
129
+ }
130
+ return result.value;
131
+ }
132
+ return response;
133
+ },
134
+ multiselect: async (message, selectOptions) => {
135
+ stdout.push(message);
136
+ selectOptions.options.forEach((choice, i) => {
137
+ const label = typeof choice === "object" ? choice.label : choice;
138
+ stdout.push(` [ ] ${i + 1}. ${label}`);
139
+ });
140
+ let response;
141
+ if (mockPromptsMap[message]) {
142
+ const responses = mockPromptsMap[message];
143
+ const usedCount = promptResponsesUsed.get(message) || 0;
144
+ if (Array.isArray(responses)) {
145
+ response = responses[usedCount] ?? responses.at(-1) ?? "";
146
+ promptResponsesUsed.set(message, usedCount + 1);
147
+ } else {
148
+ response = responses ?? "";
149
+ }
150
+ } else {
151
+ response = stdinLines.shift() || "";
152
+ }
153
+ stdout.push(`> ${response}`);
154
+ const indices = response.split(",").map((s) => Number.parseInt(s.trim(), 10) - 1);
155
+ return indices.filter((i) => i >= 0 && i < selectOptions.options.length).map((i) => {
156
+ const choice = selectOptions.options[i];
157
+ return typeof choice === "object" ? choice.value : choice;
158
+ });
159
+ }
160
+ }
161
+ );
162
+ const mockSpinner = (text) => {
163
+ if (text) {
164
+ stdout.push(`\u280B ${text}`);
165
+ }
166
+ return {
167
+ start: (text2) => {
168
+ if (text2) {
169
+ stdout.push(`\u280B ${text2}`);
170
+ }
171
+ },
172
+ stop: (text2) => {
173
+ if (text2) {
174
+ stdout.push(text2);
175
+ }
176
+ },
177
+ succeed: (text2) => {
178
+ stdout.push(`\u2705 ${text2 || "Done"}`);
179
+ },
180
+ fail: (text2) => {
181
+ stdout.push(`\u274C ${text2 || "Failed"}`);
182
+ },
183
+ warn: (text2) => {
184
+ stdout.push(`\u26A0\uFE0F ${text2 || "Warning"}`);
185
+ },
186
+ info: (text2) => {
187
+ stdout.push(`\u2139\uFE0F ${text2 || "Info"}`);
188
+ },
189
+ update: (text2) => {
190
+ stdout.push(`\u280B ${text2}`);
191
+ }
192
+ };
193
+ };
194
+ const mockShellCommands = options.mockShellCommands || {};
195
+ const mockShell = (strings, ...values) => {
196
+ const command2 = strings.reduce((acc, str, i) => {
197
+ return acc + str + (values[i] || "");
198
+ }, "").trim();
199
+ stdout.push(`$ ${command2}`);
200
+ const promise = Promise.resolve();
201
+ promise.text = async () => {
202
+ if (mockShellCommands[command2]) {
203
+ return mockShellCommands[command2];
204
+ }
205
+ if (command2.includes("git branch --show-current")) {
206
+ return "main\n";
207
+ }
208
+ if (command2.includes("git status")) {
209
+ return "nothing to commit, working tree clean\n";
210
+ }
211
+ return "";
212
+ };
213
+ promise.json = async () => {
214
+ if (mockShellCommands[command2]) {
215
+ try {
216
+ return JSON.parse(mockShellCommands[command2]);
217
+ } catch {
218
+ return {};
219
+ }
220
+ }
221
+ return {};
222
+ };
223
+ promise.quiet = () => promise;
224
+ return promise;
225
+ };
226
+ const mockColors = {
227
+ red: (text) => `[red]${text}[/red]`,
228
+ green: (text) => `[green]${text}[/green]`,
229
+ blue: (text) => `[blue]${text}[/blue]`,
230
+ yellow: (text) => `[yellow]${text}[/yellow]`,
231
+ cyan: (text) => `[cyan]${text}[/cyan]`,
232
+ magenta: (text) => `[magenta]${text}[/magenta]`,
233
+ gray: (text) => `[gray]${text}[/gray]`,
234
+ dim: (text) => `[dim]${text}[/dim]`,
235
+ bold: (text) => `[bold]${text}[/bold]`,
236
+ italic: (text) => `[italic]${text}[/italic]`,
237
+ underline: (text) => `[underline]${text}[/underline]`,
238
+ strikethrough: (text) => `[strikethrough]${text}[/strikethrough]`,
239
+ bgRed: (text) => `[bgRed]${text}[/bgRed]`,
240
+ bgGreen: (text) => `[bgGreen]${text}[/bgGreen]`,
241
+ bgBlue: (text) => `[bgBlue]${text}[/bgBlue]`,
242
+ bgYellow: (text) => `[bgYellow]${text}[/bgYellow]`,
243
+ bgCyan: (text) => `[bgCyan]${text}[/bgCyan]`,
244
+ bgMagenta: (text) => `[bgMagenta]${text}[/bgMagenta]`,
245
+ bgGray: (text) => `[bgGray]${text}[/bgGray]`,
246
+ black: (text) => `[black]${text}[/black]`,
247
+ white: (text) => `[white]${text}[/white]`,
248
+ bgBlack: (text) => `[bgBlack]${text}[/bgBlack]`,
249
+ bgWhite: (text) => `[bgWhite]${text}[/bgWhite]`,
250
+ // Add missing bright colors
251
+ brightRed: (text) => `[brightRed]${text}[/brightRed]`,
252
+ brightGreen: (text) => `[brightGreen]${text}[/brightGreen]`,
253
+ brightYellow: (text) => `[brightYellow]${text}[/brightYellow]`,
254
+ brightBlue: (text) => `[brightBlue]${text}[/brightBlue]`,
255
+ brightCyan: (text) => `[brightCyan]${text}[/brightCyan]`,
256
+ brightMagenta: (text) => `[brightMagenta]${text}[/brightMagenta]`,
257
+ brightWhite: (text) => `[brightWhite]${text}[/brightWhite]`,
258
+ reset: (text) => `[reset]${text}[/reset]`,
259
+ strip: (text) => text.replace(/\[[^\]]+\]/g, "")
260
+ };
261
+ const originalLog = console.log;
262
+ const originalError = console.error;
263
+ console.log = (...args) => {
264
+ stdout.push(args.join(" "));
265
+ };
266
+ console.error = (...args) => {
267
+ stderr.push(args.join(" "));
268
+ };
269
+ try {
270
+ const handlerArgs = {
271
+ flags: options.flags || {},
272
+ positional: options.args || [],
273
+ env: { ...process.env, ...options.env },
274
+ cwd: options.cwd || process.cwd(),
275
+ prompt: mockPrompt,
276
+ spinner: mockSpinner,
277
+ shell: mockShell,
278
+ colors: mockColors,
279
+ terminal: {
280
+ width: 80,
281
+ height: 24,
282
+ isInteractive: false,
283
+ isCI: true,
284
+ supportsColor: false,
285
+ supportsMouse: false
286
+ },
287
+ runtime: {
288
+ startTime: Date.now(),
289
+ args: options.args || [],
290
+ command: command.description
291
+ }
292
+ };
293
+ if (command.handler) {
294
+ await command.handler(handlerArgs);
295
+ }
296
+ exitCode = options.exitCode || 0;
297
+ } catch (err) {
298
+ error = err;
299
+ exitCode = 1;
300
+ stderr.push(error.message);
301
+ } finally {
302
+ console.log = originalLog;
303
+ console.error = originalError;
304
+ }
305
+ const duration = performance.now() - startTime;
306
+ return {
307
+ stdout: stdout.join("\n"),
308
+ stderr: stderr.join("\n"),
309
+ exitCode,
310
+ duration,
311
+ error
312
+ };
313
+ }
314
+ export async function testCLI(setupCLI, argv, _options = {}) {
315
+ const startTime = performance.now();
316
+ const stdout = [];
317
+ const stderr = [];
318
+ let exitCode = 0;
319
+ let error;
320
+ const originalLog = console.log;
321
+ const originalError = console.error;
322
+ const originalExit = process.exit;
323
+ console.log = (...args) => {
324
+ stdout.push(args.join(" "));
325
+ };
326
+ console.error = (...args) => {
327
+ stderr.push(args.join(" "));
328
+ };
329
+ process.exit = (code) => {
330
+ exitCode = code || 0;
331
+ throw new Error(`Process exited with code ${exitCode}`);
332
+ };
333
+ try {
334
+ const cli = await createCLI({
335
+ name: "test-cli",
336
+ version: "1.0.0",
337
+ description: "Test CLI"
338
+ });
339
+ setupCLI(cli);
340
+ await cli.run(argv);
341
+ } catch (err) {
342
+ if (!err.message.startsWith("Process exited with code")) {
343
+ error = err;
344
+ exitCode = 1;
345
+ stderr.push(error?.message || "Unknown error");
346
+ }
347
+ } finally {
348
+ console.log = originalLog;
349
+ console.error = originalError;
350
+ process.exit = originalExit;
351
+ }
352
+ const duration = performance.now() - startTime;
353
+ return {
354
+ stdout: stdout.join("\n"),
355
+ stderr: stderr.join("\n"),
356
+ exitCode,
357
+ duration,
358
+ error
359
+ };
360
+ }
@@ -0,0 +1,65 @@
1
+ import type { HandlerArgs } from "@reliverse/rempts-core";
2
+ export interface TestOptions {
3
+ /** Command flags to pass */
4
+ flags?: Record<string, unknown>;
5
+ /** Positional arguments */
6
+ args?: string[];
7
+ /** Environment variables */
8
+ env?: Record<string, string>;
9
+ /** Current working directory */
10
+ cwd?: string;
11
+ /** Input for prompts (line by line) */
12
+ stdin?: string | string[];
13
+ /** Mock prompt responses mapped by prompt message */
14
+ mockPrompts?: Record<string, string | string[]>;
15
+ /** Mock shell command outputs */
16
+ mockShellCommands?: Record<string, string>;
17
+ /** Exit code to expect */
18
+ exitCode?: number;
19
+ }
20
+ export interface TestResult {
21
+ /** Captured stdout */
22
+ stdout: string;
23
+ /** Captured stderr */
24
+ stderr: string;
25
+ /** Exit code */
26
+ exitCode: number;
27
+ /** Execution time in ms */
28
+ duration: number;
29
+ /** Any error thrown */
30
+ error?: Error;
31
+ }
32
+ export interface MockHandlerArgs extends Omit<HandlerArgs, "prompt" | "spinner" | "shell"> {
33
+ prompt: {
34
+ (message: string, options?: any): Promise<string>;
35
+ confirm: (message: string, options?: any) => Promise<boolean>;
36
+ select: <T = string>(message: string, options: any) => Promise<T>;
37
+ password: (message: string, options?: any) => Promise<string>;
38
+ multiselect: <T = string>(message: string, options: any) => Promise<T[]>;
39
+ };
40
+ spinner: (text?: string) => {
41
+ start: (text?: string) => void;
42
+ stop: (text?: string) => void;
43
+ succeed: (text?: string) => void;
44
+ fail: (text?: string) => void;
45
+ warn: (text?: string) => void;
46
+ info: (text?: string) => void;
47
+ update: (text: string) => void;
48
+ };
49
+ shell: MockShell;
50
+ }
51
+ export interface Matchers {
52
+ toHaveExitCode(code: number): void;
53
+ toHaveSucceeded(): void;
54
+ toHaveFailed(): void;
55
+ toContainInStdout(text: string): void;
56
+ toContainInStderr(text: string): void;
57
+ toMatchStdout(pattern: RegExp): void;
58
+ toMatchStderr(pattern: RegExp): void;
59
+ }
60
+ export type MockShell = (strings: TemplateStringsArray, ...values: any[]) => ShellPromise;
61
+ export interface ShellPromise extends Promise<void> {
62
+ text(): Promise<string>;
63
+ json<T = any>(): Promise<T>;
64
+ quiet(): ShellPromise;
65
+ }
package/dist/types.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@reliverse/rempts-test",
3
+ "version": "2.3.1",
4
+ "description": "Testing utilities for Rempts CLI applications",
5
+ "keywords": [
6
+ "assertions",
7
+ "bun",
8
+ "cli",
9
+ "matchers",
10
+ "rempts",
11
+ "test",
12
+ "testing",
13
+ "typescript"
14
+ ],
15
+ "homepage": "https://github.com/reliverse/dler#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/reliverse/dler/issues"
18
+ },
19
+ "license": "MIT",
20
+ "author": "blefnk",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/reliverse/dler.git",
24
+ "directory": "packages/test"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "type": "module",
30
+ "module": "./src/mod.ts",
31
+ "types": "./src/mod.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/mod.d.ts",
35
+ "import": "./dist/mod.js"
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "@reliverse/relico": "2.3.1",
43
+ "@reliverse/rempts-core": "2.3.1",
44
+ "arktype": "^2.1.29",
45
+ "arkregex": "^0.0.5"
46
+ }
47
+ }