Repository: mochajs/mocha

Problem statement:
Extension option does not clear the default .js
### Prerequisites

- [x] Checked that your issue hasn't already been filed by cross-referencing [issues with the `faq` label](https://github.com/mochajs/mocha/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq%20)
- [ x Checked next-gen ES issues and syntax problems by using the same environment and/or transpiler configuration without Mocha to ensure it isn't just a feature that actually isn't supported in the environment in question or a bug in your code.
- [x] 'Smoke tested' the code to be tested by running it outside the real test suite to get a better sense of whether the problem is in the code under test, your usage of Mocha, or Mocha itself
- [x] Ensured that there is no discrepancy between the locally and globally installed versions of Mocha. You can find them with: `node node_modules/.bin/mocha --version`(Local) and `mocha --version`(Global). We recommend that you _not_ install Mocha globally.

### Description

According to [Mocha's own docs](https://mochajs.org/#-extension-ext-watch-extensions-ext) one should be able to specify which extensions mocha will automatically load and that by specifying an extension the default of `*.js` should be cleared. _This does not happen in version 6_ (and I have never seen it happen earlier either).

### Steps to Reproduce
```
git clone fatso83/test-driven-learning
cd javascript/mocha-recursive-extensions-test
npm i
npm run test-bar
```

**Expected behavior:** 
Only this output should be shown, by Mocha loading only the file with the `.bar` extension
```
mocha --extension bar

  normal.bar
    ✓ should do nothing but register the test
```

**Actual behavior:** 
It loads the `normal.bar` - _in addition to_ all the files ending in `.js`.
```
mocha --extension bar

This is not a test: no-test.js


  normal.bar
    ✓ should do nothing but register the test

  normal.bar.js
    ✓ should do nothing but register the test

  normal.js
    ✓ should do nothing but register the test
```

**Reproduces how often:** 
Always

### Versions

6.0.2



Hints:
As far as I can tell this particular feature hasn't worked since v6. Tried on `6.0.0-0` and includes `.js` files. When run with debugger 
```
DEBUG=mocha:cli:* mocha --extension bar
```
Gives initial cli "loaded opts" as `extension: [ 'bar', 'js' ]`
Suggesting its appending a default early on. Perhaps feature was lost in migration to yargs.
I came across this issue while finding out why the latest version of Mocha was also including all .js files in the watchlist of files. #3820 seems to fix it. However the ci build seems to fail, I'll see if I can find why and fix it.

Patch:
diff --git a/lib/cli/options.js b/lib/cli/options.js
--- a/lib/cli/options.js
+++ b/lib/cli/options.js
@@ -80,11 +80,12 @@ const nargOpts = types.array
 /**
  * Wrapper around `yargs-parser` which applies our settings
  * @param {string|string[]} args - Arguments to parse
+ * @param {Object} defaultValues - Default values of mocharc.json
  * @param  {...Object} configObjects - `configObjects` for yargs-parser
  * @private
  * @ignore
  */
-const parse = (args = [], ...configObjects) => {
+const parse = (args = [], defaultValues = {}, ...configObjects) => {
   // save node-specific args for special handling.
   // 1. when these args have a "=" they should be considered to have values
   // 2. if they don't, they just boolean flags
@@ -109,6 +110,7 @@ const parse = (args = [], ...configObjects) => {
   const result = yargsParser.detailed(args, {
     configuration,
     configObjects,
+    default: defaultValues,
     coerce: coerceOpts,
     narg: nargOpts,
     alias: aliases,
@@ -324,11 +326,11 @@ const loadOptions = (argv = []) => {
 
   args = parse(
     args._,
+    mocharc,
     args,
     rcConfig || {},
     pkgConfig || {},
-    optsConfig || {},
-    mocharc
+    optsConfig || {}
   );
 
   // recombine positional arguments and "spec"
diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js
--- a/lib/cli/run-helpers.js
+++ b/lib/cli/run-helpers.js
@@ -219,7 +219,7 @@ exports.singleRun = (mocha, {files = [], exit = false} = {}) => {
  */
 exports.watchRun = (
   mocha,
-  {extension = ['js'], grep = '', ui = 'bdd', files = []} = {}
+  {extension = [], grep = '', ui = 'bdd', files = []} = {}
 ) => {
   let runner;
 
@@ -291,7 +291,7 @@ exports.watchRun = (
  */
 exports.runMocha = (
   mocha,
-  {watch = false, extension = ['js'], grep = '', ui = 'bdd', exit = false} = {},
+  {watch = false, extension = [], grep = '', ui = 'bdd', exit = false} = {},
   files = []
 ) => {
   if (watch) {
diff --git a/lib/utils.js b/lib/utils.js
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -562,7 +562,6 @@ function isHiddenOnUnix(pathname) {
  *
  * @public
  * @memberof Mocha.utils
- * @todo Fix extension handling
  * @param {string} filepath - Base path to start searching from.
  * @param {string[]} extensions - File extensions to look for.
  * @param {boolean} recursive - Whether to recurse into subdirectories.
@@ -571,13 +570,18 @@ function isHiddenOnUnix(pathname) {
  * @throws {TypeError} if `filepath` is directory and `extensions` not provided.
  */
 exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
+  extensions = extensions || [];
   var files = [];
   var stat;
 
   if (!fs.existsSync(filepath)) {
-    if (fs.existsSync(filepath + '.js')) {
-      filepath += '.js';
-    } else {
+    // check all extensions
+    extensions.forEach(function(ext) {
+      if (fs.existsSync(filepath + '.' + ext)) {
+        files.push(filepath + '.' + ext);
+      }
+    });
+    if (!files.length) {
       // Handle glob
       files = glob.sync(filepath);
       if (!files.length) {
@@ -586,8 +590,8 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
           filepath
         );
       }
-      return files;
     }
+    return files;
   }
 
   // Handle file
@@ -618,7 +622,7 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
       // ignore error
       return;
     }
-    if (!extensions) {
+    if (!extensions.length) {
       throw createMissingArgumentError(
         util.format(
           'Argument %s required when argument %s is a directory',


Test patch:
diff --git a/test/integration/file-utils.spec.js b/test/integration/file-utils.spec.js
--- a/test/integration/file-utils.spec.js
+++ b/test/integration/file-utils.spec.js
@@ -58,7 +58,7 @@ describe('file utils', function() {
       ex.and('to have length', expectedLength);
     });
 
-    it('should parse extensions from extnsions parameter', function() {
+    it('should parse extensions from extensions parameter', function() {
       var nonJsFile = tmpFile('mocha-utils-text.txt');
       fs.writeFileSync(nonJsFile, 'yippy skippy ying yang yow');
 
@@ -66,9 +66,14 @@ describe('file utils', function() {
       expect(res, 'to contain', nonJsFile).and('to have length', 1);
     });
 
-    it('should not require the extensions parameter when looking up a file', function() {
-      var res = utils.lookupFiles(tmpFile('mocha-utils'), undefined, false);
-      expect(res, 'to be', tmpFile('mocha-utils.js'));
+    it('should require the extensions parameter when looking up a file', function() {
+      var dirLookup = function() {
+        return utils.lookupFiles(tmpFile('mocha-utils'), undefined, false);
+      };
+      expect(dirLookup, 'to throw', {
+        name: 'Error',
+        code: 'ERR_MOCHA_NO_FILES_MATCH_PATTERN'
+      });
     });
 
     it('should require the extensions parameter when looking up a directory', function() {
diff --git a/test/node-unit/cli/options.spec.js b/test/node-unit/cli/options.spec.js
--- a/test/node-unit/cli/options.spec.js
+++ b/test/node-unit/cli/options.spec.js
@@ -27,7 +27,9 @@ const defaults = {
   timeout: 1000,
   timeouts: 1000,
   t: 1000,
-  opts: '/default/path/to/mocha.opts'
+  opts: '/default/path/to/mocha.opts',
+  extension: ['js'],
+  'watch-extensions': ['js']
 };
 
 describe('options', function() {
@@ -59,6 +61,7 @@ describe('options', function() {
   describe('loadOptions()', function() {
     describe('when no parameter provided', function() {
       beforeEach(function() {
+        this.timeout(500);
         readFileSync = sandbox.stub();
         readFileSync.onFirstCall().returns('{}');
         readFileSync.onSecondCall().returns('--retries 3');
@@ -497,8 +500,8 @@ describe('options', function() {
           beforeEach(function() {
             readFileSync = sandbox.stub();
             config = '/some/.mocharc.json';
-            readFileSync.onFirstCall().returns('--retries 3');
-            readFileSync.onSecondCall().returns('{}');
+            readFileSync.onFirstCall().returns('{}');
+            readFileSync.onSecondCall().returns('--retries 3');
             findConfig = sandbox.stub();
             loadConfig = sandbox.stub().throws('Error', 'failed to parse');
             findupSync = sandbox.stub().returns('/some/package.json');
@@ -542,8 +545,8 @@ describe('options', function() {
 
             beforeEach(function() {
               readFileSync = sandbox.stub();
-              readFileSync.onFirstCall().returns('--retries 3');
-              readFileSync.onSecondCall().returns('{}');
+              readFileSync.onFirstCall().returns('{}');
+              readFileSync.onSecondCall().throws();
               findConfig = sandbox.stub().returns('/some/.mocharc.json');
               loadConfig = sandbox.stub().returns({});
               findupSync = sandbox.stub().returns('/some/package.json');
@@ -578,8 +581,8 @@ describe('options', function() {
 
             beforeEach(function() {
               readFileSync = sandbox.stub();
-              readFileSync.onFirstCall().returns('--retries 3');
-              readFileSync.onSecondCall().returns('{}');
+              readFileSync.onFirstCall().returns('{}');
+              readFileSync.onSecondCall().throws();
               findConfig = sandbox.stub().returns(null);
               loadConfig = sandbox.stub().returns({});
               findupSync = sandbox.stub().returns('/some/package.json');
@@ -716,5 +719,78 @@ describe('options', function() {
         });
       });
     });
+
+    describe('"extension" handling', function() {
+      describe('when user supplies "extension" option', function() {
+        let result;
+
+        beforeEach(function() {
+          readFileSync = sandbox.stub();
+          readFileSync.onFirstCall().throws();
+          findConfig = sandbox.stub().returns('/some/.mocharc.json');
+          loadConfig = sandbox.stub().returns({extension: ['tsx']});
+          findupSync = sandbox.stub();
+          loadOptions = proxyLoadOptions({
+            readFileSync,
+            findConfig,
+            loadConfig,
+            findupSync
+          });
+          result = loadOptions(['--extension', 'ts']);
+        });
+
+        it('should not concatenate the default value', function() {
+          expect(result, 'to have property', 'extension', ['ts', 'tsx']);
+        });
+      });
+
+      describe('when user does not supply "extension" option', function() {
+        let result;
+
+        beforeEach(function() {
+          readFileSync = sandbox.stub();
+          readFileSync.onFirstCall().throws();
+          findConfig = sandbox.stub().returns('/some/.mocharc.json');
+          loadConfig = sandbox.stub().returns({});
+          findupSync = sandbox.stub();
+          loadOptions = proxyLoadOptions({
+            readFileSync,
+            findConfig,
+            loadConfig,
+            findupSync
+          });
+          result = loadOptions();
+        });
+
+        it('should retain the default', function() {
+          expect(result, 'to have property', 'extension', ['js']);
+        });
+      });
+    });
+
+    describe('"spec" handling', function() {
+      describe('when user supplies "spec" in config and positional arguments', function() {
+        let result;
+
+        beforeEach(function() {
+          readFileSync = sandbox.stub();
+          readFileSync.onFirstCall().throws();
+          findConfig = sandbox.stub().returns('/some/.mocharc.json');
+          loadConfig = sandbox.stub().returns({spec: '*.spec.js'});
+          findupSync = sandbox.stub();
+          loadOptions = proxyLoadOptions({
+            readFileSync,
+            findConfig,
+            loadConfig,
+            findupSync
+          });
+          result = loadOptions(['*.test.js']);
+        });
+
+        it('should place both into the positional arguments array', function() {
+          expect(result, 'to have property', '_', ['*.test.js', '*.spec.js']);
+        });
+      });
+    });
   });
 });