diff --git a/lib/api/_loaders/_command-loader.js b/lib/api/_loaders/_command-loader.js index 808c84bf0..f9faf8cf8 100644 --- a/lib/api/_loaders/_command-loader.js +++ b/lib/api/_loaders/_command-loader.js @@ -16,6 +16,8 @@ class BaseCommandLoader extends BaseLoader { this.ignoreUnderscoreLeadingNames = true; } + static commandRegistry = new Map(); + static isTypeImplemented(instance, method, type) { const methodTypes = method.split('|'); @@ -50,15 +52,26 @@ class BaseCommandLoader extends BaseLoader { this.nightwatchInstance.overridableCommands.add(this.commandName); } - if (this.nightwatchInstance.isApiMethodDefined(this.commandName, namespace) && !this.nightwatchInstance.overridableCommands.has(this.commandName)) { - const err = new TypeError(`Error while loading the API commands: the ${this.type} ${this.namespace || ''}.${this.commandName}() is already defined.`); + const commandKey = `${this.namespace || ''}.${this.commandName}`; + const currentFile = this.fileName; + + if (BaseCommandLoader.commandRegistry.has(commandKey)) { + const firstDefinedFile = BaseCommandLoader.commandRegistry.get(commandKey); + + const err = new TypeError( + `Error while loading the API commands: the ${this.type} ${commandKey}() is already defined.\n` + + `- First defined in: ${firstDefinedFile}\n` + + `- Current file: ${currentFile}` + ); + err.displayed = false; - err.detailedErr = `Source: ${this.fileName}`; err.showTrace = false; throw err; } + BaseCommandLoader.commandRegistry.set(commandKey, currentFile); + return this; } diff --git a/test/src/api/commands/testDuplicateCommandError.js b/test/src/api/commands/testDuplicateCommandError.js new file mode 100644 index 000000000..e19322e9f --- /dev/null +++ b/test/src/api/commands/testDuplicateCommandError.js @@ -0,0 +1,49 @@ +const assert = require('assert'); +const BaseCommandLoader = require('../../../../lib/api/_loaders/_command-loader.js'); + +describe('BaseCommandLoader - Duplicate Command Error', function () { + it('should throw an error with file paths when duplicate commands are defined', function () { + // Create two instances of the command loader to simulate commands from two different files + const loader1 = new BaseCommandLoader({}); + loader1.fileName = '/path/to/firstFile.js'; + loader1.commandName = 'testCommand'; + loader1.namespace = 'testNamespace'; + + const loader2 = new BaseCommandLoader({}); + loader2.fileName = '/path/to/secondFile.js'; + loader2.commandName = 'testCommand'; + loader2.namespace = 'testNamespace'; + + // Validate the first command, it should register without issues + loader1.validateMethod(null); + + // The second command should throw a detailed error + assert.throws( + () => loader2.validateMethod(null), + (err) => { + assert.ok(err instanceof TypeError); + assert.ok(err.message.includes('testNamespace.testCommand')); + assert.ok(err.message.includes('/path/to/firstFile.js')); + assert.ok(err.message.includes('/path/to/secondFile.js')); + + return true; + } + ); + }); + + it('should not throw an error for commands in different namespaces', function () { + const loader1 = new BaseCommandLoader({}); + loader1.fileName = '/path/to/firstFile.js'; + loader1.commandName = 'testCommand'; + loader1.namespace = 'namespace1'; + + const loader2 = new BaseCommandLoader({}); + loader2.fileName = '/path/to/secondFile.js'; + loader2.commandName = 'testCommand'; + loader2.namespace = 'namespace2'; + + // Both commands should register without issues + assert.doesNotThrow(() => loader1.validateMethod(null)); + assert.doesNotThrow(() => loader2.validateMethod(null)); + }); +});