Skip to content

Commit 63b39bb

Browse files
authored
Merge pull request #45 from constructive-io/devin/1766817855-create-test-fixture
feat(@inquirerer/test): add createTestFixture for CLI testing
2 parents 8e3406d + 7dcc812 commit 63b39bb

4 files changed

Lines changed: 2776 additions & 5113 deletions

File tree

packages/inquirerer-test/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
},
4242
"devDependencies": {
4343
"@types/jest": "^30.0.0",
44+
"@types/minimist": "^1.2.5",
4445
"jest": "^30.0.0",
4546
"makage": "0.1.8"
4647
},
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import fs from 'fs';
2+
import os from 'os';
3+
import path from 'path';
4+
import { Inquirerer, CLIOptions } from 'inquirerer';
5+
import { ParsedArgs } from 'minimist';
6+
import { createTestEnvironment, TestEnvironment } from './harness';
7+
8+
const { mkdtempSync, rmSync, cpSync } = fs;
9+
10+
/**
11+
* Configuration options for creating a CLI test fixture.
12+
*/
13+
export interface TestFixtureOptions<TResult = unknown> {
14+
/**
15+
* The CLI commands function to execute.
16+
* Should have signature: (argv, prompter, options) => Promise<TResult>
17+
*/
18+
commands: (
19+
argv: Partial<ParsedArgs>,
20+
prompter: Inquirerer,
21+
options: CLIOptions
22+
) => Promise<TResult>;
23+
24+
/**
25+
* Root directory containing test fixtures to copy from.
26+
* If not provided, no fixtures will be copied.
27+
*/
28+
fixtureRoot?: string;
29+
30+
/**
31+
* Prefix for the temporary directory name.
32+
* @default 'cli-test-'
33+
*/
34+
tmpPrefix?: string;
35+
36+
/**
37+
* Transform argv before passing to commands.
38+
* Useful for setting default values (e.g., pgpm's withInitDefaults).
39+
*/
40+
argvTransform?: (argv: Partial<ParsedArgs>) => Partial<ParsedArgs>;
41+
42+
/**
43+
* Default CLI options to merge with test environment options.
44+
*/
45+
cliOptions?: Partial<CLIOptions> & {
46+
version?: string;
47+
minimistOpts?: Record<string, unknown>;
48+
};
49+
}
50+
51+
/**
52+
* Result returned from running a CLI command in the test fixture.
53+
*/
54+
export interface RunCmdResult<TResult = unknown> {
55+
/** The result returned by the commands function */
56+
result: TResult;
57+
/** The argv that was passed to commands (after any transforms) */
58+
argv: Partial<ParsedArgs>;
59+
/** Captured output lines (ANSI stripped, key sequences humanized) */
60+
writeResults: string[];
61+
/** Captured transform stream results */
62+
transformResults: string[];
63+
}
64+
65+
/**
66+
* A test fixture for testing inquirerer-based CLI applications.
67+
*/
68+
export interface TestFixture<TResult = unknown> {
69+
/** The temporary directory created for this fixture */
70+
readonly tempDir: string;
71+
/** The directory where fixtures were copied (or tempDir if no fixtures) */
72+
readonly tempFixtureDir: string;
73+
/** The test environment with mock streams */
74+
readonly environment: TestEnvironment;
75+
76+
/**
77+
* Get the path to a file within the fixture directory.
78+
*/
79+
fixturePath(...paths: string[]): string;
80+
81+
/**
82+
* Get the path to a file within the fixture directory.
83+
* Alias for fixturePath for backwards compatibility.
84+
*/
85+
getFixturePath(...paths: string[]): string;
86+
87+
/**
88+
* Run a CLI command with the given argv.
89+
*/
90+
runCmd(argv: Partial<ParsedArgs>): Promise<RunCmdResult<TResult>>;
91+
92+
/**
93+
* Clean up the temporary directory.
94+
* Call this in afterEach or after your test completes.
95+
*/
96+
cleanup(): void;
97+
}
98+
99+
/**
100+
* Creates a test fixture for testing inquirerer-based CLI applications.
101+
*
102+
* The fixture handles:
103+
* - Creating a temporary directory
104+
* - Optionally copying fixture files
105+
* - Setting up mock stdin/stdout streams
106+
* - Creating an Inquirerer prompter with the mock streams
107+
* - Running CLI commands with the test environment
108+
*
109+
* @example
110+
* ```typescript
111+
* import { createTestFixture } from '@inquirerer/test';
112+
* import { commands } from '../src/commands';
113+
*
114+
* describe('my CLI', () => {
115+
* let fixture: TestFixture;
116+
*
117+
* beforeEach(() => {
118+
* fixture = createTestFixture({
119+
* commands,
120+
* fixtureRoot: path.resolve(__dirname, '../__fixtures__'),
121+
* tmpPrefix: 'my-cli-test-',
122+
* cliOptions: { version: '1.0.0' }
123+
* });
124+
* });
125+
*
126+
* afterEach(() => {
127+
* fixture.cleanup();
128+
* });
129+
*
130+
* it('should run a command', async () => {
131+
* const { result, writeResults } = await fixture.runCmd({ _: ['init'] });
132+
* expect(writeResults.join('')).toContain('Initialized');
133+
* });
134+
* });
135+
* ```
136+
*
137+
* @example With fixture files
138+
* ```typescript
139+
* // Copy fixtures from __fixtures__/my-project to temp dir
140+
* const fixture = createTestFixture({
141+
* commands,
142+
* fixtureRoot: FIXTURES_PATH
143+
* }, 'my-project');
144+
*
145+
* // Access files in the fixture
146+
* const configPath = fixture.fixturePath('config.json');
147+
* ```
148+
*/
149+
export function createTestFixture<TResult = unknown>(
150+
options: TestFixtureOptions<TResult>,
151+
...fixturePath: string[]
152+
): TestFixture<TResult> {
153+
const {
154+
commands,
155+
fixtureRoot,
156+
tmpPrefix = 'cli-test-',
157+
argvTransform,
158+
cliOptions = {}
159+
} = options;
160+
161+
// Create temp directory
162+
const tempDir = mkdtempSync(path.join(os.tmpdir(), tmpPrefix));
163+
164+
// Copy fixtures if provided
165+
let tempFixtureDir: string;
166+
if (fixturePath.length > 0 && fixtureRoot) {
167+
const originalFixtureDir = path.join(fixtureRoot, ...fixturePath);
168+
tempFixtureDir = path.join(tempDir, ...fixturePath);
169+
cpSync(originalFixtureDir, tempFixtureDir, { recursive: true });
170+
} else {
171+
tempFixtureDir = tempDir;
172+
}
173+
174+
// Create test environment
175+
const environment = createTestEnvironment();
176+
177+
const getFixturePath = (...paths: string[]) =>
178+
path.join(tempFixtureDir, ...paths);
179+
180+
const fixturePathFn = (...paths: string[]) =>
181+
path.join(tempFixtureDir, ...paths);
182+
183+
const cleanup = () => {
184+
rmSync(tempDir, { recursive: true, force: true });
185+
};
186+
187+
const runCmd = async (argv: Partial<ParsedArgs>): Promise<RunCmdResult<TResult>> => {
188+
const {
189+
mockInput,
190+
mockOutput,
191+
writeResults,
192+
transformResults
193+
} = environment;
194+
195+
// Apply argv transform if provided
196+
const transformedArgv = argvTransform ? argvTransform(argv) : argv;
197+
198+
// Create prompter with mock streams
199+
const prompter = new Inquirerer({
200+
input: mockInput,
201+
output: mockOutput,
202+
noTty: true
203+
});
204+
205+
// Merge CLI options
206+
const mergedOptions: CLIOptions = {
207+
noTty: true,
208+
input: mockInput,
209+
output: mockOutput,
210+
version: cliOptions.version || '1.0.0',
211+
minimistOpts: cliOptions.minimistOpts || {},
212+
...cliOptions
213+
};
214+
215+
// Run commands
216+
const result = await commands(transformedArgv, prompter, mergedOptions);
217+
218+
return {
219+
result,
220+
argv: transformedArgv,
221+
writeResults,
222+
transformResults
223+
};
224+
};
225+
226+
return {
227+
tempDir,
228+
tempFixtureDir,
229+
environment,
230+
fixturePath: fixturePathFn,
231+
getFixturePath,
232+
runCmd,
233+
cleanup
234+
};
235+
}

packages/inquirerer-test/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export type { KeySequence } from './keys';
66
export { setupTests, createTestEnvironment } from './harness';
77
export type { TestEnvironment, InputResponse } from './harness';
88

9+
// Test fixture for CLI testing
10+
export { createTestFixture } from './fixture';
11+
export type { TestFixture, TestFixtureOptions, RunCmdResult } from './fixture';
12+
913
// Snapshot utilities
1014
export { normalizePackageJsonForSnapshot } from './snapshot';
1115
export type { NormalizeOptions } from './snapshot';

0 commit comments

Comments
 (0)