import { exec } from 'child_process'; import { describe, it, expect, test } from 'vitest'; function runCommand(command: string): Promise { return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { reject(stderr); } else { resolve(stdout); } }); }); } const cliTools = ['t', 'citty', 'cac', 'commander']; describe.each(cliTools)('cli completion tests for %s', (cliTool) => { const commandPrefix = `pnpm tsx examples/demo.${cliTool}.ts complete --`; it('should complete cli options', async () => { const output = await runCommand(`${commandPrefix}`); expect(output).toMatchSnapshot(); }); describe('cli option completion tests', () => { const optionTests = [ { partial: '--p', expected: '--port' }, { partial: '-p', expected: '-p' }, // Test short flag completion { partial: '-H', expected: '-H' }, // Test another short flag completion ]; test.each(optionTests)( "should complete option for partial input '%s'", async ({ partial }) => { const command = `${commandPrefix} dev ${partial}`; const output = await runCommand(command); expect(output).toMatchSnapshot(); } ); }); describe('cli option exclusion tests', () => { const alreadySpecifiedTests = [ { specified: '--config', shouldNotContain: '--config' }, ]; test.each(alreadySpecifiedTests)( "should not suggest already specified option '%s'", async ({ specified, shouldNotContain }) => { const command = `${commandPrefix} ${specified} --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); } ); }); describe('cli option value handling', () => { it('should resolve port value correctly', async () => { const command = `${commandPrefix} dev --port=3`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should not show duplicate options', async () => { const command = `${commandPrefix} --config vite.config.js --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should resolve config option values correctly', async () => { const command = `${commandPrefix} --config vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should handle unknown options with no completions', async () => { const command = `${commandPrefix} --unknownoption`; const output = await runCommand(command); expect(output.trim()).toMatchSnapshot(); }); }); describe('boolean option handling', () => { it('should complete subcommands and arguments after boolean options', async () => { const command = `${commandPrefix} dev --verbose ""`; const output = await runCommand(command); // After a boolean option, should show subcommands/arguments (not flag values) expect(output).toContain('build'); expect(output).toContain('start'); expect(output).not.toContain('verbose'); }); it('should complete subcommands and arguments after short boolean options', async () => { const command = `${commandPrefix} dev -v ""`; const output = await runCommand(command); // After a short boolean option, should show subcommands/arguments (not flag values) expect(output).toContain('build'); expect(output).toContain('start'); expect(output).not.toContain('verbose'); }); it('should not interfere with command completion after boolean options', async () => { const command = `${commandPrefix} dev --verbose s`; const output = await runCommand(command); // Should complete subcommands that start with 's' even after a boolean option expect(output).toContain('start'); }); it('should not interfere with option completion after boolean options', async () => { const command = `${commandPrefix} dev --verbose --h`; const output = await runCommand(command); // Should complete options that start with '--h' even after a boolean option expect(output).toContain('--host'); }); }); describe('option API overload tests', () => { it('should handle basic option (name + description only) as boolean flag', async () => { // This tests the case: option('quiet', 'Suppress output') const command = `${commandPrefix} dev --quiet ""`; const output = await runCommand(command); // Should be treated as boolean flag — shows subcommands, not flag values expect(output).toContain('build'); expect(output).not.toContain('quiet'); }); it('should handle option with alias only as boolean flag', async () => { // This tests the case: option('verbose', 'Enable verbose', 'v') const command = `${commandPrefix} dev --verbose ""`; const output = await runCommand(command); // Should be treated as boolean flag — shows subcommands, not flag values expect(output).toContain('build'); expect(output).not.toContain('verbose'); }); it('should handle option with alias only (short flag) as boolean flag', async () => { // This tests the short flag version: -v instead of --verbose const command = `${commandPrefix} dev -v ""`; const output = await runCommand(command); // Should be treated as boolean flag — shows subcommands, not flag values expect(output).toContain('build'); expect(output).not.toContain('verbose'); }); it('should handle option with handler only as value option', async () => { // This tests the case: option('port', 'Port number', handlerFunction) const command = `${commandPrefix} dev --port ""`; const output = await runCommand(command); // Should provide value completions because it has a handler expect(output).toContain('3000'); expect(output).toContain('8080'); }); it('should handle option with both handler and alias as value option', async () => { // This tests the case: option('config', 'Config file', handlerFunction, 'c') const command = `${commandPrefix} --config ""`; const output = await runCommand(command); // Should provide value completions because it has a handler expect(output).toContain('vite.config.ts'); expect(output).toContain('vite.config.js'); }); it('should handle option with both handler and alias (short flag) as value option', async () => { // This tests the short flag version with handler: -c instead of --config const command = `${commandPrefix} -c ""`; const output = await runCommand(command); // Should provide value completions because it has a handler expect(output).toContain('vite.config.ts'); expect(output).toContain('vite.config.js'); }); it('should correctly detect boolean vs value options in mixed scenarios', async () => { // Test that boolean options don't interfere with value options const command = `${commandPrefix} dev --verbose --port ""`; const output = await runCommand(command); // Should complete port values, not be confused by preceding boolean flag expect(output).toContain('3000'); expect(output).toContain('8080'); }); it('should correctly handle aliases for different option types', async () => { // Mix of boolean flag with alias (-v) and value option with alias (-p) const command = `${commandPrefix} dev -v -p ""`; const output = await runCommand(command); // Should complete port values via short flag expect(output).toContain('3000'); expect(output).toContain('8080'); }); }); describe('--config option tests', () => { it('should complete --config option values', async () => { const command = `${commandPrefix} --config ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete --config option with partial input', async () => { const command = `${commandPrefix} --config vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete --config option with equals sign', async () => { const command = `${commandPrefix} --config=vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete short flag -c option values', async () => { const command = `${commandPrefix} -c ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete short flag -c option with partial input', async () => { const command = `${commandPrefix} -c vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should not suggest --config after it has been used', async () => { const command = `${commandPrefix} --config vite.config.ts --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); describe('root command argument tests', () => { it('should complete root command project argument', async () => { const command = `${commandPrefix} ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command project argument with partial input', async () => { const command = `${commandPrefix} my-`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command project argument after options', async () => { const command = `${commandPrefix} --config vite.config.ts ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command project argument with options and partial input', async () => { const command = `${commandPrefix} --mode development my-`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); describe('root command option tests', () => { it('should complete root command --mode option values', async () => { const command = `${commandPrefix} --mode ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command --mode option with partial input', async () => { const command = `${commandPrefix} --mode dev`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command --logLevel option values', async () => { const command = `${commandPrefix} --logLevel ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command --logLevel option with partial input', async () => { const command = `${commandPrefix} --logLevel i`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command short flag -m option values', async () => { const command = `${commandPrefix} -m ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command short flag -l option values', async () => { const command = `${commandPrefix} -l ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command options after project argument', async () => { const command = `${commandPrefix} my-app --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete root command options with partial input after project argument', async () => { const command = `${commandPrefix} my-app --m`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); describe('edge case completions for end with space', () => { it('should suggest port values if user ends with space after `--port`', async () => { const command = `${commandPrefix} dev --port ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { const command = `${commandPrefix} dev --po`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { const command = `${commandPrefix} dev --port=`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); describe('short flag handling', () => { it('should handle short flag value completion', async () => { const command = `${commandPrefix} dev -p `; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should handle short flag with equals sign', async () => { const command = `${commandPrefix} dev -p=3`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should handle global short flags', async () => { const command = `${commandPrefix} -c `; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should not show duplicate options when short flag is used', async () => { const command = `${commandPrefix} -c vite.config.js --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); describe('positional argument completions', () => { it('should complete multiple positional arguments when ending with space', async () => { const command = `${commandPrefix} lint ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete multiple positional arguments when ending with part of the value', async () => { const command = `${commandPrefix} lint ind`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete single positional argument when ending with space', async () => { const command = `${commandPrefix} lint main.ts ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); describe('copy command argument handlers', () => { it('should complete source argument with directory suggestions', async () => { const command = `${commandPrefix} copy ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete destination argument with build suggestions', async () => { const command = `${commandPrefix} copy src/ ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should filter source suggestions when typing partial input', async () => { const command = `${commandPrefix} copy s`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should filter destination suggestions when typing partial input', async () => { const command = `${commandPrefix} copy src/ b`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); describe('lint command argument handlers', () => { it('should complete files argument with file suggestions', async () => { const command = `${commandPrefix} lint ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should filter file suggestions when typing partial input', async () => { const command = `${commandPrefix} lint m`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should continue completing variadic files argument after first file', async () => { const command = `${commandPrefix} lint main.ts ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should continue completing variadic suggestions after first file', async () => { const command = `${commandPrefix} lint main.ts i`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); }); }); // Add specific tests for Commander describe('commander specific tests', () => { it('should complete commands', async () => { const command = `pnpm tsx examples/demo.commander.ts complete -- `; const output = await runCommand(command); expect(output).toContain('serve'); expect(output).toContain('build'); expect(output).toContain('deploy'); }); it('should handle subcommands', async () => { // Check subcommands of root command. const command1 = `pnpm tsx examples/demo.commander.ts complete -- deploy`; const output1 = await runCommand(command1); expect(output1).toContain('deploy'); expect(output1).toContain('Deploy the application'); // Check subcommands of subcommand. const command2 = `pnpm tsx examples/demo.commander.ts complete -- deploy ""`; const output2 = await runCommand(command2); expect(output2).toContain('staging'); expect(output2).toContain('production'); }); it('should intercept completion when using parseAsync', async () => { const command = `pnpm tsx examples/demo.commander-async.ts complete -- `; const output = await runCommand(command); expect(output).toContain('greet'); expect(output).toContain('Say hello'); // directive line must be present, proving t.parse was invoked expect(output).toMatch(/:\d+\s*$/); }); }); describe('shell completion script generation', () => { const shells = ['zsh', 'bash', 'fish', 'powershell']; const cliTool = 'commander'; // Use commander for shell script generation tests test.each(shells)('should generate %s completion script', async (shell) => { const command = `pnpm tsx examples/demo.${cliTool}.ts complete ${shell}`; const output = await runCommand(command); expect(output).toContain(`# ${shell} completion for`); expect(output.length).toBeGreaterThan(100); // Ensure we got a substantial script }); });