diff --git a/lib/cli/watch-run.js b/lib/cli/watch-run.js index 3bac550389..559329647e 100644 --- a/lib/cli/watch-run.js +++ b/lib/cli/watch-run.js @@ -41,6 +41,9 @@ exports.watchParallelRun = ( // I don't know why we're cloning the root suite. const rootSuite = mocha.suite.clone(); + // ensure we aren't leaking event listeners + mocha.dispose(); + // this `require` is needed because the require cache has been cleared. the dynamic // exports set via the below call to `mocha.ui()` won't work properly if a // test depends on this module (see `required-tokens.spec.js`). @@ -100,6 +103,9 @@ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => { // I don't know why we're cloning the root suite. const rootSuite = mocha.suite.clone(); + // ensure we aren't leaking event listeners + mocha.dispose(); + // this `require` is needed because the require cache has been cleared. the dynamic // exports set via the below call to `mocha.ui()` won't work properly if a // test depends on this module (see `required-tokens.spec.js`). diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 5435b1ddee..ccb20c9b11 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -415,8 +415,12 @@ async function runMochaWatchAsync(args, opts, change) { if (typeof opts === 'string') { opts = {cwd: opts}; } - opts = {sleepMs: 2000, ...opts, fork: process.platform === 'win32'}; - opts.stdio = ['pipe', 'pipe', 'inherit']; + opts = { + sleepMs: 2000, + stdio: ['pipe', 'pipe', 'inherit'], + ...opts, + fork: process.platform === 'win32' + }; const [mochaProcess, resultPromise] = invokeMochaAsync( [...args, '--watch'], opts @@ -539,24 +543,25 @@ function sleep(time) { module.exports = { DEFAULT_FIXTURE, SPLIT_DOT_REPORTER_REGEXP, + copyFixture, createTempDir, + escapeRegExp, + getSummary, invokeMocha, invokeMochaAsync, invokeNode, - getSummary, + replaceFileContents, resolveFixturePath, - toJSONResult, - escapeRegExp, runMocha, - runMochaJSON, runMochaAsync, + runMochaJSON, runMochaJSONAsync, runMochaWatchAsync, runMochaWatchJSONAsync, - copyFixture, - touchFile, - replaceFileContents + sleep, + toJSONResult, + touchFile }; /** diff --git a/test/integration/options/watch.spec.js b/test/integration/options/watch.spec.js index 0211ca25c2..498ddcf5f9 100644 --- a/test/integration/options/watch.spec.js +++ b/test/integration/options/watch.spec.js @@ -5,6 +5,8 @@ const path = require('path'); const { copyFixture, runMochaWatchJSONAsync, + sleep, + runMochaWatchAsync, touchFile, replaceFileContents, createTempDir, @@ -343,5 +345,33 @@ describe('--watch', function() { it('mochaHooks.afterAll runs as expected', setupHookTest('afterAll')); it('mochaHooks.afterEach runs as expected', setupHookTest('afterEach')); }); + + it('should not leak event listeners', function() { + this.timeout(20000); + const testFile = path.join(tempDir, 'test.js'); + copyFixture(DEFAULT_FIXTURE, testFile); + + return expect( + runMochaWatchAsync( + [testFile], + {cwd: tempDir, stdio: 'pipe'}, + async () => { + // we want to cause _n + 1_ reruns, which should cause the warning + // to occur if the listeners aren't properly destroyed + const iterations = new Array(process.getMaxListeners() + 1); + // eslint-disable-next-line no-unused-vars + for await (const _ of iterations) { + touchFile(testFile); + await sleep(1000); + } + } + ), + 'when fulfilled', + 'to satisfy', + { + output: expect.it('not to match', /MaxListenersExceededWarning/) + } + ); + }); }); });