Repository: mochajs/mocha

Problem statement:
Thrown error in afterEach as a root hook plugin is ignored in watchmode
<!--
Have you read Mocha's Code of Conduct? By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/mochajs/mocha/blob/master/.github/CODE_OF_CONDUCT.md
For more, check out the Mocha Gitter chat room: https://gitter.im/mochajs/mocha

Detail the steps necessary to reproduce the problem. To get the fastest support, create an MCVE and upload it to GitHub.
create an [MCVE](https://stackoverflow.com/help/mcve) and upload it to GitHub.
-->

### Prerequisites

<!--
Place an `x` between the square brackets on the lines below for every satisfied prerequisite.
-->

- [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)
- ~[ ] 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.~ Not syntax related
- ~[ ] '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~ Not related to code under test
- [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

<!--
[Description of the issue]
-->

### Steps to Reproduce

Full repro: https://github.com/eps1lon/repro-mocha-root-hook-throw

```js
// test.js
it("root hooks tests onces", () => {});
it("root hooks tests twice", () => {});

// `hooks.js`
module.exports = {
  mochaHooks: {
    afterEach() {
      throw new Error("Threw in root afterEach");
    },
  },
};

// .mocharc.js
module.exports = {
  require: [require.resolve("./hooks")],
};
```

- `mocha test.js` fails as expected
- `mocha test.js --watch` passes unexpectedly

**Expected behavior:**

afterEach as a root hook behave the same as "local" hooks with regard to thrown errors.

**Actual behavior:**

Thrown error is ignored in afterEach in watchmode

**Reproduces how often:** 

All the time

### Versions

<!-- If applicable, please specify: -->

- The output of `mocha --version` and `node node_modules/.bin/mocha --version`: 8.0.1
  `mocha --version` does not work because I didn't install it globally
- The output of `node --version`: 12.16.2
- Your operating system
  - name and version: Ubuntu 18.04.4 LTS
  - architecture (32 or 64-bit): 64bit
- Your shell (e.g., bash, zsh, PowerShell, cmd): `fish, version 2.7.1`
- Your browser and version (if running browser tests): N/A
- Any third-party Mocha-related modules (and their versions): N/A
- Any code transpiler (e.g., TypeScript, CoffeeScript, Babel) being used (and its version): N/A

### Additional Information

N/A


Hints:
This might be the same root problem as #4344.  
One more related issue: console logs are ignored in root hook plugins in watchmode.
that sounds like a bug
Hit this issue myself recently. I did a little digging & issue seems to be caused by the cloning the suite on L40.

https://github.com/mochajs/mocha/blob/02bdb6bc6c029fb0c01389b27797d00faa76ddde/lib/cli/watch-run.js#L38-L66

It looks like `suite.clone()` doesn't copy over any of the hooks.

https://github.com/mochajs/mocha/blob/7d3151d08a3082dd022116d00234caf1600a71b5/lib/suite.js#L117-L127

I did a quick local patch to add these & watch worked as expected with root hooks.

Patch:
diff --git a/lib/cli/watch-run.js b/lib/cli/watch-run.js
--- a/lib/cli/watch-run.js
+++ b/lib/cli/watch-run.js
@@ -59,6 +59,10 @@ exports.watchParallelRun = (
       // in `createRerunner`), we need to call `mocha.ui()` again to set up the context/globals.
       newMocha.ui(newMocha.options.ui);
 
+      // we need to call `newMocha.rootHooks` to set up rootHooks for the new
+      // suite
+      newMocha.rootHooks(newMocha.options.rootHooks);
+
       // in parallel mode, the main Mocha process doesn't actually load the
       // files. this flag prevents `mocha.run()` from autoloading.
       newMocha.lazyLoadFiles(true);
@@ -118,6 +122,10 @@ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => {
       // in `createRerunner`), we need to call `mocha.ui()` again to set up the context/globals.
       newMocha.ui(newMocha.options.ui);
 
+      // we need to call `newMocha.rootHooks` to set up rootHooks for the new
+      // suite
+      newMocha.rootHooks(newMocha.options.rootHooks);
+
       return newMocha;
     },
     afterRun({watcher}) {


Test patch:
diff --git a/test/integration/fixtures/options/watch/hook.fixture.js b/test/integration/fixtures/options/watch/hook.fixture.js
new file mode 100644
--- /dev/null
+++ b/test/integration/fixtures/options/watch/hook.fixture.js
@@ -0,0 +1,7 @@
+module.exports = {
+  mochaHooks: {
+    ["<hook>"]: function() {
+      throw new Error("<hook> Hook Error");
+    },
+  },
+};
diff --git a/test/integration/options/watch.spec.js b/test/integration/options/watch.spec.js
--- a/test/integration/options/watch.spec.js
+++ b/test/integration/options/watch.spec.js
@@ -275,6 +275,43 @@ describe('--watch', function() {
         expect(results[1].tests, 'to have length', 2);
       });
     });
+
+    describe('with required hooks', function() {
+      /**
+       * Helper for setting up hook tests
+       *
+       * @param {string} hookName name of hook to test
+       * @return {function}
+       */
+      function setupHookTest(hookName) {
+        return function() {
+          const testFile = path.join(tempDir, 'test.js');
+          const hookFile = path.join(tempDir, 'hook.js');
+
+          copyFixture('__default__', testFile);
+          copyFixture('options/watch/hook', hookFile);
+
+          replaceFileContents(hookFile, '<hook>', hookName);
+
+          return runMochaWatch(
+            [testFile, '--require', hookFile],
+            tempDir,
+            () => {
+              touchFile(testFile);
+            }
+          ).then(results => {
+            expect(results.length, 'to equal', 2);
+            expect(results[0].failures, 'to have length', 1);
+            expect(results[1].failures, 'to have length', 1);
+          });
+        };
+      }
+
+      it('mochaHooks.beforeAll runs as expected', setupHookTest('beforeAll'));
+      it('mochaHooks.beforeEach runs as expected', setupHookTest('beforeEach'));
+      it('mochaHooks.afterAll runs as expected', setupHookTest('afterAll'));
+      it('mochaHooks.afterEach runs as expected', setupHookTest('afterEach'));
+    });
   });
 });