Repository: karma-runner/karma
Pull request #3618 (2021-01-14 07:23:22)

chore(test): run unit tests without grunt wrapper

karmarunnerbot: :white_check_mark: [Build karma 464 completed](https://ci.appveyor.com/project/karmarunnerbot/karma/builds/37258799) (commit https://github.com/karma-runner/karma/commit/345758996e by @devoto13)
karmarunnerbot: :white_check_mark: [Build karma 463 completed](https://ci.appveyor.com/project/karmarunnerbot/karma-8n04t/builds/37258801) (commit https://github.com/karma-runner/karma/commit/345758996e by @devoto13)
karmarunnerbot: :white_check_mark: [Build karma 466 completed](https://ci.appveyor.com/project/karmarunnerbot/karma/builds/37280981) (commit https://github.com/karma-runner/karma/commit/1aa3ddb8f3 by @devoto13)
karmarunnerbot: :white_check_mark: [Build karma 465 completed](https://ci.appveyor.com/project/karmarunnerbot/karma-8n04t/builds/37280983) (commit https://github.com/karma-runner/karma/commit/1aa3ddb8f3 by @devoto13)

Patch:
diff --git a/docs/config/02-files.md b/docs/config/02-files.md
index f85c4d77..aac9304e 100644
--- a/docs/config/02-files.md
+++ b/docs/config/02-files.md
@@ -1,28 +1,14 @@
-**The `files` array determines which files are included in the browser and which files are watched and served by Karma.**
+The `files` array determines which files are included in the browser, watched, and served by Karma.
 
-
-## Pattern matching and `basePath`
-- All of the relative patterns will get resolved using the `basePath` first.
-- If the `basePath` is a relative path, it gets resolved to the
-  directory where the configuration file is located.
-- Eventually, all the patterns will get resolved into files using
-  [glob], so you can use [minimatch] expressions like `test/unit/**/*.spec.js`.
-
-
-## Ordering
-- The order of patterns determines the order in which files are included in the browser.
-- Multiple files matching a single pattern are sorted alphabetically.
-- Each file is included exactly once. If multiple patterns match the
-  same file, it's included as if it only matched the first pattern.
-
-
-## Included, served, watched
-Each pattern is either a simple string or an object with the following properties:
+## `files`
+**Type.** Array
+**No Default.** This property is mandatory.
+**Description.** Each item is either a string (equivalent to `{ pattern: "<string>" }`) or an object with the following properties:
 
 ### `pattern`
 * **Type.** String
 * **No Default.** This property is mandatory.
-* **Description.** The pattern to use for matching.
+* **Description.** The pattern to use for matching. See below for details on how patterns are resolved.
 
 ### `type`
 * **Type.** String
@@ -68,6 +54,18 @@ Each pattern is either a simple string or an object with the following propertie
 * **Default.** `false`
 * **Description.** Should the files be served from disk on each request by Karma's webserver?
 
+## Pattern matching and `basePath`
+- All of the relative patterns will get resolved using the `basePath` first.
+- If the `basePath` is a relative path, it gets resolved to the
+  directory where the configuration file is located.
+- Eventually, all the patterns will get resolved into files using
+  [glob], so you can use [minimatch] expressions like `test/unit/**/*.spec.js`.
+
+## Ordering
+- The order of patterns determines the order in which files are included in the browser.
+- Multiple files matching a single pattern are sorted alphabetically.
+- Each file is included exactly once. If multiple patterns match the
+  same file, it's included as if it only matched the first pattern.
 
 ## Preprocessor transformations
 Depending on preprocessor configuration, be aware that files loaded may be transformed and no longer available in
@@ -100,6 +98,30 @@ files: [
 ],
 ```
 
+## Loading files from another server
+
+Pattern can also be an absolute URL. This allows including files which are not served by Karma.
+
+Example:
+
+```javascript
+config.set({
+  files: [
+    'https://example.com/my-lib.js',
+    { pattern: 'https://example.com/my-lib', type: 'js' }
+  ]
+})
+```
+
+Absolute URLs have some special rules comparing to the regular file paths:
+
+- Globing is not support, so each URL must be specified as a separate pattern.
+- Most of the regular options are not supported:
+    - `watched` is always `false`
+    - `included` is always `true`
+    - `served` is always `false`
+    - `nocache` is always `false`
+
 ## Loading Assets
 By default all assets are served at `http://localhost:[PORT]/base/`
 
diff --git a/gruntfile.js b/gruntfile.js
deleted file mode 100644
index 40da12be..00000000
--- a/gruntfile.js
+++ /dev/null
@@ -1,22 +0,0 @@
-module.exports = function (grunt) {
-  grunt.initConfig({
-    mochaTest: {
-      options: {
-        reporter: 'dot',
-        ui: 'bdd',
-        quiet: false,
-        colors: true
-      },
-      unit: {
-        src: [
-          'test/unit/mocha-globals.js',
-          'test/unit/**/*.spec.js'
-        ]
-      }
-    }
-  })
-
-  grunt.loadNpmTasks('grunt-mocha-test')
-
-  grunt.registerTask('default', ['mochaTest:unit'])
-}
diff --git a/lib/server.js b/lib/server.js
index ff03553e..0cf94ce9 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -43,7 +43,9 @@ function createSocketIoServer (webServer, executor, config) {
     transports: config.transports,
     forceJSONP: config.forceJSONP,
     // Default is 5000 in socket.io v2.x and v3.x.
-    pingTimeout: config.pingTimeout || 5000
+    pingTimeout: config.pingTimeout || 5000,
+    // Default in v2 is 1e8 and coverage results can fail at 1e6
+    maxHttpBufferSize: 1e8
   })
 
   // hack to overcome circular dependency
@@ -82,6 +84,8 @@ class Server extends KarmaEventEmitter {
       filesPromise: ['factory', createFilesPromise],
       socketServer: ['factory', createSocketIoServer],
       executor: ['factory', Executor.factory],
+      // TODO: Deprecated. Remove in the next major
+      customFileHandlers: ['value', []],
       reporter: ['factory', reporter.createReporters],
       capturedBrowsers: ['factory', BrowserCollection.factory],
       args: ['value', {}],
diff --git a/lib/web-server.js b/lib/web-server.js
index 37981843..394066b7 100644
--- a/lib/web-server.js
+++ b/lib/web-server.js
@@ -16,6 +16,25 @@ const proxyMiddleware = require('./middleware/proxy')
 
 const log = require('./logger').create('web-server')
 
+function createCustomHandler (customFileHandlers, config) {
+  let warningDone = false
+
+  return function (request, response, next) {
+    const handler = customFileHandlers.find((handler) => handler.urlRegex.test(request.url))
+
+    if (customFileHandlers.length > 0 && !warningDone) {
+      warningDone = true
+      log.warn('The `customFileHandlers` is deprecated and will be removed in Karma 7. Please upgrade plugins relying on this provider.')
+    }
+
+    return handler
+      ? handler.handler(request, response, 'fake/static', 'fake/adapter', config.basePath, 'fake/root')
+      : next()
+  }
+}
+
+createCustomHandler.$inject = ['customFileHandlers', 'config']
+
 function createFilesPromise (emitter, fileList) {
   // Set an empty list of files to avoid race issues with
   // file_list_modified not having been emitted yet
@@ -58,6 +77,8 @@ function createWebServer (injector, config) {
   handler.use(injector.invoke(sourceFilesMiddleware.create))
   // TODO(vojta): extract the proxy into a plugin
   handler.use(proxyMiddlewareInstance)
+  // TODO: Deprecated. Remove in the next major
+  handler.use(injector.invoke(createCustomHandler))
 
   if (config.middleware) {
     config.middleware.forEach((middleware) => handler.use(injector.get('middleware:' + middleware)))
diff --git a/package-lock.json b/package-lock.json
index b92a4b0a..69090efe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2416,12 +2416,6 @@
         "through": ">=2.2.7 <3"
       }
     },
-    "abbrev": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
-      "dev": true
-    },
     "accepts": {
       "version": "1.3.7",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -2599,12 +2593,6 @@
       "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
       "dev": true
     },
-    "array-each": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
-      "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=",
-      "dev": true
-    },
     "array-find-index": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
@@ -2634,12 +2622,6 @@
         "is-string": "^1.0.5"
       }
     },
-    "array-slice": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
-      "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
-      "dev": true
-    },
     "array-unique": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
@@ -4164,12 +4146,6 @@
         "minimalistic-assert": "^1.0.0"
       }
     },
-    "detect-file": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
-      "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
-      "dev": true
-    },
     "detective": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
@@ -4912,12 +4888,6 @@
         }
       }
     },
-    "eventemitter2": {
-      "version": "0.4.14",
-      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
-      "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=",
-      "dev": true
-    },
     "eventemitter3": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
@@ -5021,12 +4991,6 @@
         }
       }
     },
-    "exit": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
-      "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
-      "dev": true
-    },
     "expand-brackets": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -5062,15 +5026,6 @@
         }
       }
     },
-    "expand-tilde": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
-      "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
-      "dev": true,
-      "requires": {
-        "homedir-polyfill": "^1.0.1"
-      }
-    },
     "ext": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
@@ -5357,49 +5312,6 @@
         }
       }
     },
-    "findup-sync": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
-      "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
-      "dev": true,
-      "requires": {
-        "glob": "~5.0.0"
-      },
-      "dependencies": {
-        "glob": {
-          "version": "5.0.15",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
-          "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
-          "dev": true,
-          "requires": {
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "2 || 3",
-            "once": "^1.3.0",
-            "path-is-absolute": "^1.0.0"
-          }
-        }
-      }
-    },
-    "fined": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
-      "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
-      "dev": true,
-      "requires": {
-        "expand-tilde": "^2.0.2",
-        "is-plain-object": "^2.0.3",
-        "object.defaults": "^1.1.0",
-        "object.pick": "^1.2.0",
-        "parse-filepath": "^1.0.1"
-      }
-    },
-    "flagged-respawn": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
-      "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
-      "dev": true
-    },
     "flat-cache": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
@@ -5457,15 +5369,6 @@
       "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
       "dev": true
     },
-    "for-own": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
-      "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
-      "dev": true,
-      "requires": {
-        "for-in": "^1.0.1"
-      }
-    },
     "form-data": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.0.tgz",
@@ -5582,12 +5485,6 @@
       "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
       "dev": true
     },
-    "getobject": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz",
-      "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=",
-      "dev": true
-    },
     "gherkin": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/gherkin/-/gherkin-5.0.0.tgz",
@@ -5649,30 +5546,6 @@
         "ini": "^1.3.4"
       }
     },
-    "global-modules": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
-      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
-      "dev": true,
-      "requires": {
-        "global-prefix": "^1.0.1",
-        "is-windows": "^1.0.1",
-        "resolve-dir": "^1.0.0"
-      }
-    },
-    "global-prefix": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
-      "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
-      "dev": true,
-      "requires": {
-        "expand-tilde": "^2.0.2",
-        "homedir-polyfill": "^1.0.1",
-        "ini": "^1.3.4",
-        "is-windows": "^1.0.1",
-        "which": "^1.2.14"
-      }
-    },
     "globals": {
       "version": "12.4.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
@@ -5731,147 +5604,6 @@
       "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
       "dev": true
     },
-    "grunt": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.2.1.tgz",
-      "integrity": "sha512-zgJjn9N56tScvRt/y0+1QA+zDBnKTrkpyeSBqQPLcZvbqTD/oyGMrdZQXmm6I3828s+FmPvxc3Xv+lgKFtudOw==",
-      "dev": true,
-      "requires": {
-        "dateformat": "~3.0.3",
-        "eventemitter2": "~0.4.13",
-        "exit": "~0.1.2",
-        "findup-sync": "~0.3.0",
-        "glob": "~7.1.6",
-        "grunt-cli": "~1.3.2",
-        "grunt-known-options": "~1.1.0",
-        "grunt-legacy-log": "~2.0.0",
-        "grunt-legacy-util": "~1.1.1",
-        "iconv-lite": "~0.4.13",
-        "js-yaml": "~3.14.0",
-        "minimatch": "~3.0.4",
-        "mkdirp": "~1.0.4",
-        "nopt": "~3.0.6",
-        "rimraf": "~3.0.2"
-      },
-      "dependencies": {
-        "dateformat": {
-          "version": "3.0.3",
-          "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
-          "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
-          "dev": true
-        },
-        "js-yaml": {
-          "version": "3.14.0",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
-          "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
-          "dev": true,
-          "requires": {
-            "argparse": "^1.0.7",
-            "esprima": "^4.0.0"
-          }
-        },
-        "mkdirp": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
-          "dev": true
-        }
-      }
-    },
-    "grunt-cli": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz",
-      "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==",
-      "dev": true,
-      "requires": {
-        "grunt-known-options": "~1.1.0",
-        "interpret": "~1.1.0",
-        "liftoff": "~2.5.0",
-        "nopt": "~4.0.1",
-        "v8flags": "~3.1.1"
-      },
-      "dependencies": {
-        "nopt": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
-          "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
-          "dev": true,
-          "requires": {
-            "abbrev": "1",
-            "osenv": "^0.1.4"
-          }
-        }
-      }
-    },
-    "grunt-known-options": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz",
-      "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==",
-      "dev": true
-    },
-    "grunt-legacy-log": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz",
-      "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==",
-      "dev": true,
-      "requires": {
-        "colors": "~1.1.2",
-        "grunt-legacy-log-utils": "~2.0.0",
-        "hooker": "~0.2.3",
-        "lodash": "~4.17.5"
-      },
-      "dependencies": {
-        "colors": {
-          "version": "1.1.2",
-          "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
-          "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
-          "dev": true
-        }
-      }
-    },
-    "grunt-legacy-log-utils": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz",
-      "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==",
-      "dev": true,
-      "requires": {
-        "chalk": "~2.4.1",
-        "lodash": "~4.17.10"
-      }
-    },
-    "grunt-legacy-util": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz",
-      "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==",
-      "dev": true,
-      "requires": {
-        "async": "~1.5.2",
-        "exit": "~0.1.1",
-        "getobject": "~0.1.0",
-        "hooker": "~0.2.3",
-        "lodash": "~4.17.10",
-        "underscore.string": "~3.3.4",
-        "which": "~1.3.0"
-      },
-      "dependencies": {
-        "async": {
-          "version": "1.5.2",
-          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
-          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
-          "dev": true
-        }
-      }
-    },
-    "grunt-mocha-test": {
-      "version": "0.13.3",
-      "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.13.3.tgz",
-      "integrity": "sha512-zQGEsi3d+ViPPi7/4jcj78afKKAKiAA5n61pknQYi25Ugik+aNOuRmiOkmb8mN2CeG8YxT+YdT1H1Q7B/eNkoQ==",
-      "dev": true,
-      "requires": {
-        "hooker": "^0.2.3",
-        "mkdirp": "^0.5.0"
-      }
-    },
     "hard-rejection": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@@ -5974,27 +5706,12 @@
         "minimalistic-crypto-utils": "^1.0.1"
       }
     },
-    "homedir-polyfill": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
-      "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
-      "dev": true,
-      "requires": {
-        "parse-passwd": "^1.0.0"
-      }
-    },
     "hook-std": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz",
       "integrity": "sha512-zZ6T5WcuBMIUVh49iPQS9t977t7C0l7OtHrpeMb5uk48JdflRX0NSFvCekfYNmGQETnLq9W/isMyHl69kxGi8g==",
       "dev": true
     },
-    "hooker": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
-      "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=",
-      "dev": true
-    },
     "hosted-git-info": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
@@ -6387,12 +6104,6 @@
         "xtend": "^4.0.0"
       }
     },
-    "interpret": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
-      "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
-      "dev": true
-    },
     "into-stream": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz",
@@ -6403,16 +6114,6 @@
         "p-is-promise": "^3.0.0"
       }
     },
-    "is-absolute": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
-      "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
-      "dev": true,
-      "requires": {
-        "is-relative": "^1.0.0",
-        "is-windows": "^1.0.1"
-      }
-    },
     "is-accessor-descriptor": {
       "version": "0.1.6",
       "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
@@ -6603,15 +6304,6 @@
         "has-symbols": "^1.0.1"
       }
     },
-    "is-relative": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
-      "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
-      "dev": true,
-      "requires": {
-        "is-unc-path": "^1.0.0"
-      }
-    },
     "is-running": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz",
@@ -6648,15 +6340,6 @@
         "text-extensions": "^1.0.0"
       }
     },
-    "is-unc-path": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
-      "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
-      "dev": true,
-      "requires": {
-        "unc-path-regex": "^0.1.2"
-      }
-    },
     "is-windows": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -6948,45 +6631,6 @@
         "type-check": "~0.4.0"
       }
     },
-    "liftoff": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz",
-      "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=",
-      "dev": true,
-      "requires": {
-        "extend": "^3.0.0",
-        "findup-sync": "^2.0.0",
-        "fined": "^1.0.1",
-        "flagged-respawn": "^1.0.0",
-        "is-plain-object": "^2.0.4",
-        "object.map": "^1.0.0",
-        "rechoir": "^0.6.2",
-        "resolve": "^1.1.7"
-      },
-      "dependencies": {
-        "findup-sync": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
-          "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
-          "dev": true,
-          "requires": {
-            "detect-file": "^1.0.0",
-            "is-glob": "^3.1.0",
-            "micromatch": "^3.0.4",
-            "resolve-dir": "^1.0.1"
-          }
-        },
-        "is-glob": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
-          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
-          "dev": true,
-          "requires": {
-            "is-extglob": "^2.1.0"
-          }
-        }
-      }
-    },
     "lines-and-columns": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -7195,15 +6839,6 @@
       "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==",
       "dev": true
     },
-    "make-iterator": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
-      "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
-      "dev": true,
-      "requires": {
-        "kind-of": "^6.0.2"
-      }
-    },
     "map-cache": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -7699,15 +7334,6 @@
       "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
       "dev": true
     },
-    "nopt": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
-      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
-      "dev": true,
-      "requires": {
-        "abbrev": "1"
-      }
-    },
     "normalize-package-data": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@@ -11352,28 +10978,6 @@
         "object-keys": "^1.0.11"
       }
     },
-    "object.defaults": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
-      "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=",
-      "dev": true,
-      "requires": {
-        "array-each": "^1.0.1",
-        "array-slice": "^1.0.0",
-        "for-own": "^1.0.0",
-        "isobject": "^3.0.0"
-      }
-    },
-    "object.map": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
-      "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=",
-      "dev": true,
-      "requires": {
-        "for-own": "^1.0.0",
-        "make-iterator": "^1.0.0"
-      }
-    },
     "object.pick": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
@@ -11443,12 +11047,6 @@
       "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
       "dev": true
     },
-    "os-homedir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
-      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
-      "dev": true
-    },
     "os-name": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
@@ -11465,22 +11063,6 @@
       "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=",
       "dev": true
     },
-    "os-tmpdir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
-      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
-      "dev": true
-    },
-    "osenv": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
-      "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
-      "dev": true,
-      "requires": {
-        "os-homedir": "^1.0.0",
-        "os-tmpdir": "^1.0.0"
-      }
-    },
     "outpipe": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz",
@@ -11604,17 +11186,6 @@
         "safe-buffer": "^5.1.1"
       }
     },
-    "parse-filepath": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
-      "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=",
-      "dev": true,
-      "requires": {
-        "is-absolute": "^1.0.0",
-        "map-cache": "^0.2.0",
-        "path-root": "^0.1.1"
-      }
-    },
     "parse-json": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@@ -11624,12 +11195,6 @@
         "error-ex": "^1.2.0"
       }
     },
-    "parse-passwd": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
-      "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
-      "dev": true
-    },
     "parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -11682,21 +11247,6 @@
       "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=",
       "dev": true
     },
-    "path-root": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
-      "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=",
-      "dev": true,
-      "requires": {
-        "path-root-regex": "^0.1.0"
-      }
-    },
-    "path-root-regex": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
-      "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=",
-      "dev": true
-    },
     "path-to-regexp": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
@@ -12122,15 +11672,6 @@
         }
       }
     },
-    "rechoir": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
-      "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
-      "dev": true,
-      "requires": {
-        "resolve": "^1.1.6"
-      }
-    },
     "redeyed": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz",
@@ -12214,16 +11755,6 @@
         "path-parse": "^1.0.6"
       }
     },
-    "resolve-dir": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
-      "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
-      "dev": true,
-      "requires": {
-        "expand-tilde": "^2.0.0",
-        "global-modules": "^1.0.0"
-      }
-    },
     "resolve-from": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -13744,12 +13275,6 @@
       "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==",
       "dev": true
     },
-    "unc-path-regex": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
-      "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
-      "dev": true
-    },
     "undeclared-identifiers": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz",
@@ -13763,16 +13288,6 @@
         "xtend": "^4.0.1"
       }
     },
-    "underscore.string": {
-      "version": "3.3.5",
-      "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz",
-      "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==",
-      "dev": true,
-      "requires": {
-        "sprintf-js": "^1.0.3",
-        "util-deprecate": "^1.0.2"
-      }
-    },
     "union-value": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -13958,15 +13473,6 @@
       "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==",
       "dev": true
     },
-    "v8flags": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz",
-      "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==",
-      "dev": true,
-      "requires": {
-        "homedir-polyfill": "^1.0.1"
-      }
-    },
     "validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
diff --git a/package.json b/package.json
index 9fdce2ca..9507faf3 100644
--- a/package.json
+++ b/package.json
@@ -449,9 +449,6 @@
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-promise": "^4.2.1",
     "eslint-plugin-standard": "^4.0.1",
-    "grunt": "^1.2.1",
-    "grunt-cli": "^1.1.0",
-    "grunt-mocha-test": "^0.13.2",
     "http2": "^3.3.6",
     "husky": "^4.2.5",
     "jasmine-core": "^3.6.0",
@@ -496,7 +493,7 @@
   "scripts": {
     "lint": "eslint . --ext js --ignore-pattern *.tpl.js",
     "lint:fix": "eslint . --ext js --ignore-pattern *.tpl.js --fix",
-    "test:unit": "grunt",
+    "test:unit": "mocha \"test/unit/**/*.spec.js\"",
     "test:e2e": "cucumber-js test/e2e/*.feature",
     "test:client": "node bin/karma start test/client/karma.conf.js",
     "test": "npm run test:unit && npm run test:e2e && npm run test:client",


Test patch:
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 00000000..58a295fd
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1,3 @@
+--reporter dot
+--ui bdd
+test/unit/mocha-globals.js
diff --git a/test/unit/web-server.spec.js b/test/unit/web-server.spec.js
index 8517f172..fccf57b6 100644
--- a/test/unit/web-server.spec.js
+++ b/test/unit/web-server.spec.js
@@ -31,7 +31,7 @@ describe('web-server', () => {
   // NOTE(vojta): only loading once, to speed things up
   // this relies on the fact that none of these tests mutate fs
   const m = mocks.loadFile(path.join(__dirname, '/../../lib/web-server.js'), _mocks, _globals)
-  server = emitter = null
+  let customFileHandlers = server = emitter = null
   let beforeMiddlewareActive = false
   let middlewareActive = false
   const servedFiles = (files) => {
@@ -40,6 +40,7 @@ describe('web-server', () => {
 
   describe('request', () => {
     beforeEach(() => {
+      customFileHandlers = []
       emitter = new EventEmitter()
       const config = {
         basePath: '/base/path',
@@ -56,6 +57,7 @@ describe('web-server', () => {
 
       const injector = new di.Injector([{
         config: ['value', config],
+        customFileHandlers: ['value', customFileHandlers],
         emitter: ['value', emitter],
         fileList: ['value', { files: { served: [], included: [] } }],
         filesPromise: ['factory', m.createFilesPromise],
@@ -180,6 +182,22 @@ describe('web-server', () => {
         })
     })
 
+    it('should load custom handlers', () => {
+      servedFiles(new Set())
+
+      customFileHandlers.push({
+        urlRegex: /\/some\/weird/,
+        handler (request, response, staticFolder, adapterFolder, baseFolder, urlRoot) {
+          response.writeHead(222)
+          response.end('CONTENT')
+        }
+      })
+
+      return request(server)
+        .get('/some/weird/url')
+        .expect(222, 'CONTENT')
+    })
+
     it('should serve 404 for non-existing files', () => {
       servedFiles(new Set())
 
@@ -196,6 +214,7 @@ describe('web-server', () => {
         cert: fs.readFileSync(path.join(__dirname, '/certificates/server.crt'))
       }
 
+      customFileHandlers = []
       emitter = new EventEmitter()
 
       const injector = new di.Injector([{
@@ -206,6 +225,7 @@ describe('web-server', () => {
           httpsServerOptions: credentials,
           client: { useIframe: true, useSingleWindow: false }
         }],
+        customFileHandlers: ['value', customFileHandlers],
         emitter: ['value', emitter],
         fileList: ['value', { files: { served: [], included: [] } }],
         filesPromise: ['factory', m.createFilesPromise],
@@ -244,10 +264,12 @@ describe('web-server', () => {
         cert: fs.readFileSync(path.join(__dirname, '/certificates/server.crt'))
       }
 
+      customFileHandlers = []
       emitter = new EventEmitter()
 
       const injector = new di.Injector([{
         config: ['value', { basePath: '/base/path', urlRoot: '/', httpModule: http2, protocol: 'https:', httpsServerOptions: credentials }],
+        customFileHandlers: ['value', customFileHandlers],
         emitter: ['value', emitter],
         fileList: ['value', { files: { served: [], included: [] } }],
         filesPromise: ['factory', m.createFilesPromise],