diff --git a/index.js b/index.js index ce38da26b..42caa05e6 100644 --- a/index.js +++ b/index.js @@ -360,8 +360,12 @@ Command.prototype.action = function(fn) { fn.apply(self, actionArgs); }; var parent = this.parent || this; - var name = parent === this ? '*' : this._name; - parent.on('command:' + name, listener); + if (parent === this) { + parent.on('program-command', listener); + } else { + parent.on('command:' + this._name, listener); + } + if (this._alias) parent.on('command:' + this._alias, listener); return this; }; @@ -798,19 +802,19 @@ Command.prototype.parseArgs = function(args, unknown) { if (this.listeners('command:' + name).length) { this.emit('command:' + args.shift(), args, unknown); } else { + this.emit('program-command', args, unknown); this.emit('command:*', args, unknown); } } else { outputHelpIfNecessary(this, unknown); - // If there were no args and we have unknown options, // then they are extraneous and we need to error. if (unknown.length > 0 && !this.defaultExecutable) { this.unknownOption(unknown[0]); } - if (this.commands.length === 0 && - this._args.filter(function(a) { return a.required; }).length === 0) { - this.emit('command:*'); + // Call the program action handler, unless it has a (missing) required parameter and signature does not match. + if (this._args.filter(function(a) { return a.required; }).length === 0) { + this.emit('program-command'); } } diff --git a/tests/command.action.test.js b/tests/command.action.test.js index fa0f85c35..ebe9f3caf 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -35,7 +35,7 @@ test('when .action called with extra arguments then extras also passed to action expect(actionMock).toHaveBeenCalledWith('my-file', cmd, ['a']); }); -test('when .action on program with argument then action called', () => { +test('when .action on program with required argument and argument supplied then action called', () => { const actionMock = jest.fn(); const program = new commander.Command(); program @@ -45,6 +45,16 @@ test('when .action on program with argument then action called', () => { expect(actionMock).toHaveBeenCalledWith('my-file', program); }); +test('when .action on program with required argument and argument not supplied then action not called', () => { + const actionMock = jest.fn(); + const program = new commander.Command(); + program + .arguments('') + .action(actionMock); + program.parse(['node', 'test']); + expect(actionMock).not.toHaveBeenCalled(); +}); + // Changes made in #729 to call program action handler test('when .action on program and no arguments then action called', () => { const actionMock = jest.fn(); @@ -75,7 +85,7 @@ test('when .action on program without optional argument supplied then action cal expect(actionMock).toHaveBeenCalledWith(undefined, program); }); -test('when .action on program with subcommand and program argument then program action called', () => { +test('when .action on program with optional argument and subcommand and program argument then program action called', () => { const actionMock = jest.fn(); const program = new commander.Command(); program @@ -88,3 +98,18 @@ test('when .action on program with subcommand and program argument then program expect(actionMock).toHaveBeenCalledWith('a', program); }); + +// Changes made in #1062 to allow this case +test('when .action on program with optional argument and subcommand and no program argument then program action called', () => { + const actionMock = jest.fn(); + const program = new commander.Command(); + program + .arguments('[file]') + .action(actionMock); + program + .command('subcommand'); + + program.parse(['node', 'test']); + + expect(actionMock).toHaveBeenCalledWith(undefined, program); +}); diff --git a/tests/command.asterisk.test.js b/tests/command.asterisk.test.js index 44a8690f2..19d0257f9 100644 --- a/tests/command.asterisk.test.js +++ b/tests/command.asterisk.test.js @@ -1,34 +1,102 @@ const commander = require('../'); -test('when no arguments then asterisk action not called', () => { - const mockAction = jest.fn(); - const program = new commander.Command(); - program - .command('install') - .action(mockAction); - program.parse(['node', 'test']); - expect(mockAction).not.toHaveBeenCalled(); -}); +// .command('*') is the old main/default command handler. It adds a listener +// for 'command:*'. It has been somewhat replaced by the program action handler, +// so most uses are probably old code. Current plan is keep the code backwards compatible +// and put work in elsewhere for new code (e.g. evolving behaviour for program action handler). +// +// The event 'command:*' is also listened for directly for testing for unknown commands +// due to an example in the README, although this is not robust (e.g. sent for git-style commands). +// +// Historical: the event 'command:*' used to also be shared by the action handler on the program. + +describe(".command('*')", () => { + test('when no arguments then asterisk action not called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .command('*') + .action(mockAction); + program.parse(['node', 'test']); + expect(mockAction).not.toHaveBeenCalled(); + }); + + test('when unrecognised argument then asterisk action called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .command('*') + .action(mockAction); + program.parse(['node', 'test', 'unrecognised-command']); + expect(mockAction).toHaveBeenCalled(); + }); + + test('when recognised command then asterisk action not called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .command('install') + .action(() => { }); + program + .command('*') + .action(mockAction); + program.parse(['node', 'test', 'install']); + expect(mockAction).not.toHaveBeenCalled(); + }); -test('when recognised command then asterisk action not called', () => { - const mockAction = jest.fn(); - const program = new commander.Command(); - program - .command('install') - .action(() => { }); - program - .action(mockAction); - program.parse(['node', 'test', 'install']); - expect(mockAction).not.toHaveBeenCalled(); + test('when unrecognised command/argument then asterisk action called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .command('install'); + program + .command('*') + .action(mockAction); + program.parse(['node', 'test', 'unrecognised-command']); + expect(mockAction).toHaveBeenCalled(); + }); }); -test('when unrecognised command/argument then asterisk action called', () => { - const mockAction = jest.fn(); - const program = new commander.Command(); - program - .command('install'); - program - .action(mockAction); - program.parse(['node', 'test', 'unrecognised-command']); - expect(mockAction).toHaveBeenCalled(); +// Test .on explicitly rather than assuming covered by .command +describe(".on('command:*')", () => { + test('when no arguments then listener not called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .on('command:*', mockAction); + program.parse(['node', 'test']); + expect(mockAction).not.toHaveBeenCalled(); + }); + + test('when unrecognised argument then listener called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .on('command:*', mockAction); + program.parse(['node', 'test', 'unrecognised-command']); + expect(mockAction).toHaveBeenCalled(); + }); + + test('when recognised command then listener not called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .command('install') + .action(() => { }); + program + .on('command:*', mockAction); + program.parse(['node', 'test', 'install']); + expect(mockAction).not.toHaveBeenCalled(); + }); + + test('when unrecognised command/argument then listener called', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .command('install'); + program + .on('command:*', mockAction); + program.parse(['node', 'test', 'unrecognised-command']); + expect(mockAction).toHaveBeenCalled(); + }); });