Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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');
}
}

Expand Down
29 changes: 27 additions & 2 deletions tests/command.action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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('<file>')
.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();
Expand Down Expand Up @@ -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
Expand All @@ -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);
});
124 changes: 96 additions & 28 deletions tests/command.asterisk.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});