diff --git a/.travis.yml b/.travis.yml index 7c1e548d8..e6bee9aca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,9 @@ jobs: - node_js: "14" script: - npm run init - - commitlint-travis + - if [[ "$TRAVIS_BRANCH" != "master" ]]; then + commitlint-travis; + fi; - npm run lint - npm run build:check - npm run test:unit diff --git a/CHANGELOG.md b/CHANGELOG.md index 1556bc8d0..3fff01afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ +# [6.2.0](https://github.com/karma-runner/karma/compare/v6.1.2...v6.2.0) (2021-03-10) + + +### Features + +* **plugins:** add support wildcard config for scoped package plugin ([#3659](https://github.com/karma-runner/karma/issues/3659)) ([39831b1](https://github.com/karma-runner/karma/commit/39831b1c2f9cbeebdba94c73ce353efb7c44e802)) + +## [6.1.2](https://github.com/karma-runner/karma/compare/v6.1.1...v6.1.2) (2021-03-09) + + +### Bug Fixes + +* **commitlint:** skip task on master ([#3650](https://github.com/karma-runner/karma/issues/3650)) ([3fc6fda](https://github.com/karma-runner/karma/commit/3fc6fdadd6b0ed6838de048c15485b1bd815fe23)) +* patch karma to allow loading virtual packages ([#3663](https://github.com/karma-runner/karma/issues/3663)) ([5bfcf5f](https://github.com/karma-runner/karma/commit/5bfcf5f37de6f0a12abcf9914c2fad510395b4d6)) + +## [6.1.1](https://github.com/karma-runner/karma/compare/v6.1.0...v6.1.1) (2021-02-12) + + +### Bug Fixes + +* **config:** check extension before ts-node register ([#3651](https://github.com/karma-runner/karma/issues/3651)) ([474f4e1](https://github.com/karma-runner/karma/commit/474f4e1caff469cce87f19a11d9179e4e05552f9)), closes [#3329](https://github.com/karma-runner/karma/issues/3329) +* report launcher process error when exit event is not emitted ([#3647](https://github.com/karma-runner/karma/issues/3647)) ([7ab86be](https://github.com/karma-runner/karma/commit/7ab86be25c334b07747632b0a6bdb1d650d881bc)) + +# [6.1.0](https://github.com/karma-runner/karma/compare/v6.0.4...v6.1.0) (2021-02-03) + + +### Features + +* **config:** improve `karma.config.parseConfig` error handling ([#3635](https://github.com/karma-runner/karma/issues/3635)) ([9dba1e2](https://github.com/karma-runner/karma/commit/9dba1e20af48d4885e1a1c6da8c08454acb0db9d)) + +## [6.0.4](https://github.com/karma-runner/karma/compare/v6.0.3...v6.0.4) (2021-02-01) + + +### Bug Fixes + +* **cli:** temporarily disable strict parameters validation ([#3641](https://github.com/karma-runner/karma/issues/3641)) ([9c755e0](https://github.com/karma-runner/karma/commit/9c755e0d61f1e8fb0fed1281fc8a331d5f1734be)), closes [#3625](https://github.com/karma-runner/karma/issues/3625) +* **client:** fix a false positive page reload error in Safari ([#3643](https://github.com/karma-runner/karma/issues/3643)) ([2a57b23](https://github.com/karma-runner/karma/commit/2a57b230cd6b27e1a6e903ca6557c5a6b3e31bf6)) +* ensure that Karma supports running tests on IE 11 ([#3642](https://github.com/karma-runner/karma/issues/3642)) ([dbd1943](https://github.com/karma-runner/karma/commit/dbd1943e6901c4cb86280db7663afde32f9ab86c)) + +## [6.0.3](https://github.com/karma-runner/karma/compare/v6.0.2...v6.0.3) (2021-01-27) + + +### Bug Fixes + +* **plugins:** refactor instantiatePlugin from preproprocessor ([#3628](https://github.com/karma-runner/karma/issues/3628)) ([e02858a](https://github.com/karma-runner/karma/commit/e02858ae0d0de3f05add976b10e4b6b935cc3dd7)) + +## [6.0.2](https://github.com/karma-runner/karma/compare/v6.0.1...v6.0.2) (2021-01-25) + + +### Bug Fixes + +* avoid ES6+ syntax in client scripts ([#3629](https://github.com/karma-runner/karma/issues/3629)) ([6629e96](https://github.com/karma-runner/karma/commit/6629e96901dbeae24fbaa4d0bfa009618fb8ee75)), closes [#3630](https://github.com/karma-runner/karma/issues/3630) + +## [6.0.1](https://github.com/karma-runner/karma/compare/v6.0.0...v6.0.1) (2021-01-20) + + +### Bug Fixes + +* **server:** set maxHttpBufferSize to the socket.io v2 default ([#3626](https://github.com/karma-runner/karma/issues/3626)) ([69baddc](https://github.com/karma-runner/karma/commit/69baddc843e4852a6770bfc1212fc2bce1f38fe7)), closes [#3621](https://github.com/karma-runner/karma/issues/3621) +* restore `customFileHandlers` provider ([#3624](https://github.com/karma-runner/karma/issues/3624)) ([25d9abb](https://github.com/karma-runner/karma/commit/25d9abb76929b6ea8abe1cf040ba6db2f269d50e)) + # [6.0.0](https://github.com/karma-runner/karma/compare/v5.2.3...v6.0.0) (2021-01-13) diff --git a/client/karma.js b/client/karma.js index 0734822ff..5586bdc9e 100644 --- a/client/karma.js +++ b/client/karma.js @@ -5,7 +5,6 @@ var util = require('../common/util') function Karma (updater, socket, iframe, opener, navigator, location, document) { this.updater = updater var startEmitted = false - var karmaNavigating = false var self = this var queryParams = util.parseQueryParams(location.search) var browserId = queryParams.id || util.generateId('manual-') @@ -83,21 +82,21 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) var childWindow = null function navigateContextTo (url) { - karmaNavigating = true if (self.config.useIframe === false) { // run in new window if (self.config.runInParent === false) { // If there is a window already open, then close it // DEV: In some environments (e.g. Electron), we don't have setter access for location if (childWindow !== null && childWindow.closed !== true) { + // The onbeforeunload listener was added by context to catch + // unexpected navigations while running tests. + childWindow.onbeforeunload = undefined childWindow.close() } childWindow = opener(url) - karmaNavigating = false // run context on parent element (client_with_context) // using window.__karma__.scriptUrls to get the html element strings and load them dynamically } else if (url !== 'about:blank') { - karmaNavigating = false var loadScript = function (idx) { if (idx < window.__karma__.scriptUrls.length) { var parser = new DOMParser() @@ -128,15 +127,10 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) } // run in iframe } else { + // The onbeforeunload listener was added by the context to catch + // unexpected navigations while running tests. + iframe.contentWindow.onbeforeunload = undefined iframe.src = policy.createURL(url) - karmaNavigating = false - } - } - - this.onbeforeunload = function () { - if (!karmaNavigating) { - // TODO(vojta): show what test (with explanation about jasmine.UPDATE_INTERVAL) - self.error('Some of your tests did a full page reload!') } } @@ -191,7 +185,7 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) } socket.emit('karma_error', message) - self.updater.updateTestStatus(`karma_error ${message}`) + self.updater.updateTestStatus('karma_error ' + message) this.complete() return false } @@ -240,8 +234,9 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) // A test could have incorrectly issued a navigate. Wait one turn // to ensure the error from an incorrect navigate is processed. - setTimeout(() => { - if (this.config.clearContext) { + var config = this.config + setTimeout(function () { + if (config.clearContext) { navigateContextTo('about:blank') } diff --git a/client/updater.js b/client/updater.js index 3365b5253..569709e3d 100644 --- a/client/updater.js +++ b/client/updater.js @@ -29,7 +29,7 @@ function StatusUpdater (socket, titleElement, bannerElement, browsersElement) { if (!titleElement || !bannerElement) { return } - titleElement.textContent = `Karma v ${VERSION} - ${connectionText}; test: ${testText}; ${pingText}` + titleElement.textContent = 'Karma v ' + VERSION + ' - ' + connectionText + '; test: ' + testText + '; ' + pingText bannerElement.className = connectionText === 'connected' ? 'online' : 'offline' } @@ -46,32 +46,32 @@ function StatusUpdater (socket, titleElement, bannerElement, browsersElement) { updateBanner() } - socket.on('connect', () => { + socket.on('connect', function () { updateConnectionStatus('connected') }) - socket.on('disconnect', () => { + socket.on('disconnect', function () { updateConnectionStatus('disconnected') }) - socket.on('reconnecting', (sec) => { - updateConnectionStatus(`reconnecting in ${sec} seconds`) + socket.on('reconnecting', function (sec) { + updateConnectionStatus('reconnecting in ' + sec + ' seconds') }) - socket.on('reconnect', () => { + socket.on('reconnect', function () { updateConnectionStatus('reconnected') }) - socket.on('reconnect_failed', () => { + socket.on('reconnect_failed', function () { updateConnectionStatus('reconnect_failed') }) socket.on('info', updateBrowsersInfo) - socket.on('disconnect', () => { + socket.on('disconnect', function () { updateBrowsersInfo([]) }) - socket.on('ping', () => { + socket.on('ping', function () { updatePingStatus('ping...') }) - socket.on('pong', (latency) => { - updatePingStatus(`ping ${latency}ms`) + socket.on('pong', function (latency) { + updatePingStatus('ping ' + latency + 'ms') }) return { updateTestStatus: updateTestStatus } diff --git a/context/karma.js b/context/karma.js index 677767c9a..859d56d19 100644 --- a/context/karma.js +++ b/context/karma.js @@ -77,9 +77,9 @@ function ContextKarma (callParentKarmaMethod) { contextWindow.onerror = function () { return self.error.apply(self, arguments) } - // DEV: We must defined a function since we don't want to pass the event object - contextWindow.onbeforeunload = function (e, b) { - callParentKarmaMethod('onbeforeunload', []) + + contextWindow.onbeforeunload = function () { + return self.error('Some of your tests did a full page reload!') } contextWindow.dump = function () { diff --git a/docs/config/01-configuration-file.md b/docs/config/01-configuration-file.md index 24646e6c4..6d1c73f53 100644 --- a/docs/config/01-configuration-file.md +++ b/docs/config/01-configuration-file.md @@ -548,12 +548,9 @@ mime: { **Default:** `['karma-*']` -**Description:** List of plugins to load. A plugin can be a string (in which case it will be required by Karma) or an inlined plugin - Object. -By default, Karma loads all sibling NPM modules which have a name starting with `karma-*`. +**Description:** List of plugins to load. A plugin can be either a plugin object, or a string containing name of the module which exports a plugin object. See [plugins] for more information on how to install and use plugins. -Note: Just about all plugins in Karma require an additional library to be installed (via NPM). - -See [plugins] for more information. +By default, Karma loads plugins from all sibling NPM packages which have a name starting with `karma-*`. ## port @@ -587,8 +584,7 @@ If, after test execution or after Karma attempts to kill the browser, browser is Preprocessors can be loaded through [plugins]. -Note: Just about all preprocessors in Karma (other than CoffeeScript and some other defaults) -require an additional library to be installed (via NPM). +Note: Just about all preprocessors in Karma require an additional library to be installed (via NPM). Be aware that preprocessors may be transforming the files and file types that are available at run time. For instance, if you are using the "coverage" preprocessor on your source files, if you then attempt to interactively debug diff --git a/docs/config/02-files.md b/docs/config/02-files.md index f85c4d772..aac9304e0 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: "" }`) 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/docs/config/03-browsers.md b/docs/config/03-browsers.md index 0f69fa12e..2ae47e05e 100644 --- a/docs/config/03-browsers.md +++ b/docs/config/03-browsers.md @@ -16,7 +16,7 @@ Note: Most of the browser launchers need to be loaded as [plugins]. - [JSDOM](https://www.npmjs.com/package/karma-jsdom-launcher) - [Opera](https://www.npmjs.com/package/karma-opera-launcher) - [Internet Explorer](https://www.npmjs.com/package/karma-ie-launcher) -- [SauceLabs](https://www.npmjs.com/package/karma-saucelabs-launcher) +- [SauceLabs](https://www.npmjs.com/package/karma-sauce-launcher) - [BrowserStack](https://www.npmjs.com/package/karma-browserstack-launcher) - [many more](https://www.npmjs.org/browse/keyword/karma-launcher) diff --git a/docs/config/05-plugins.md b/docs/config/05-plugins.md index 6272fc80f..4cf8b9203 100644 --- a/docs/config/05-plugins.md +++ b/docs/config/05-plugins.md @@ -1,11 +1,12 @@ -Karma can be easily extended through plugins. -In fact, all the existing preprocessors, reporters, browser launchers and frameworks are also plugins. +Karma can be easily extended through plugins. In fact, all the existing preprocessors, reporters, browser launchers and frameworks are plugins. -## Installation +You can install [existing plugins] from NPM or you can write [your own plugins][developing plugins] for Karma. -Karma plugins are NPM modules, so the recommended way to install them are as project dependencies in your `package.json`: +## Installing Plugins -```javascript +The recommended way to install plugins is to add them as project dependencies in your `package.json`: + +```json { "devDependencies": { "karma": "~0.10", @@ -22,26 +23,35 @@ Therefore, a simple way to install a plugin is: npm install karma- --save-dev ``` - ## Loading Plugins -By default, Karma loads all sibling NPM modules which have a name starting with `karma-*`. -You can also explicitly list plugins you want to load via the `plugins` configuration setting. The configuration value can either be -a string (module name), which will be required by Karma, or an object (inlined plugin). +By default, Karma loads plugins from all sibling NPM packages which have a name starting with `karma-*`. + +You can also override this behavior and explicitly list plugins you want to load via the `plugins` configuration setting: ```javascript -plugins: [ - // Karma will require() these plugins - 'karma-jasmine', - 'karma-chrome-launcher' - - // inlined plugins - {'framework:xyz': ['factory', factoryFn]}, - require('./plugin-required-from-config') -] +config.set({ + plugins: [ + // Load a plugin you installed from NPM. + require('karma-jasmine'), + + // Load a plugin from the file in your project. + require('./my-custom-plugin'), + + // Define a plugin inline. + { 'framework:xyz': ['factory', factoryFn] }, + + // Specify a module name or path which Karma will require() and load its + // default export as a plugin. + 'karma-chrome-launcher', + './my-fancy-plugin' + ] +}) ``` -There are already many [existing plugins]. Of course, you can write [your own plugins] too! +## Activating Plugins + +Adding a plugin to the `plugins` array only makes Karma aware of the plugin, but it does not activate it. Depending on the plugin type you'll need to add a plugin name into `frameworks`, `reporters`, `preprocessors`, `middleware` or `browsers` configuration key to activate it. For the detailed information refer to the corresponding plugin documentation or check out [Developing plugins][developing plugins] guide for more in-depth explanation of how plugins work. [existing plugins]: https://npmjs.org/browse/keyword/karma-plugin -[your own plugins]: ../dev/plugins.html +[developing plugins]: ../dev/plugins.html diff --git a/docs/dev/05-plugins.md b/docs/dev/05-plugins.md index d9371e46a..d7cae9e6d 100644 --- a/docs/dev/05-plugins.md +++ b/docs/dev/05-plugins.md @@ -1,61 +1,109 @@ pageTitle: Developing Plugins -Karma can be extended through plugins. A plugin is essentially an NPM module. Typically, there are four kinds of plugins: **frameworks**, **reporters**, **launchers** and **preprocessors**. The best way to understand how this works is to take a look at some of the existing plugins. Following sections list some of the plugins that you might use as a reference. +Karma can be extended through plugins. There are five kinds of plugins: *framework*, *reporter*, *launcher*, *preprocessor* and *middleware*. Each type allows to modify a certain aspect of the Karma behavior. -## Frameworks -- example plugins: [karma-jasmine], [karma-mocha], [karma-requirejs] -- use naming convention is `karma-*` -- use NPM keywords `karma-plugin`, `karma-framework`. +- A *framework* connects a testing framework (like Mocha) to a Karma API, so browser can send test results back to a Karma server. +- A *reporter* defines how test results are reported to a user. +- A *launcher* allows Karma to launch different browsers to run tests in. +- A *preprocessor* is responsible for transforming/transpiling source files before loading them into a browser. +- A *middleware* can be used to customise how files are served to a browser. -## Reporters -- example plugins: [karma-growl-reporter], [karma-junit-reporter], [karma-material-reporter] -- use naming convention is `karma-*-reporter` -- use NPM keywords `karma-plugin`, `karma-reporter` +## Dependency injection -## Launchers -- example plugins: [karma-chrome-launcher], [karma-sauce-launcher] -- use naming convention is `karma-*-launcher` -- use NPM keywords `karma-plugin`, `karma-launcher` +Karma is assembled using [*dependency injection*](https://en.wikipedia.org/wiki/Dependency_injection). It is important to understand this concept to be able to develop plugins. -## Preprocessors +On the very high level you can think of Karma as an object where each key (a *DI token*) is mapped to a certain Karma object (a *service*). For example, `config` DI token maps to `Config` instance, which holds current Karma configuration. Plugins can request (or *inject*) various Karma objects by specifying a corresponding DI token. Upon injection a plugin can interact with injected services to implement their functionality. -A preprocessor is a function that accepts three arguments (`content`, `file`, and `next`), mutates the content in some way, and passes it on to the next preprocessor. +There is no exhaustive list of all available services and their DI tokens, but you can discover them by reading Karma's or other plugins' source code. -- arguments passed to preprocessor plugins: - - **`content`** of the file being processed - - **`file`** object describing the file being processed - - **path:** the current file, mutable file path. e. g. `some/file.coffee` -> `some/file.coffee.js` _This path is mutable and may not actually exist._ - - **originalPath:** the original, unmutated path - - **encodings:** A mutable, keyed object where the keys are a valid encoding type ('gzip', 'compress', 'br', etc.) and the values are the encoded content. Encoded content should be stored here and not resolved using `next(null, encodedContent)` - - **type:** determines how to include a file, when serving - - **`next`** function to be called when preprocessing is complete, should be called as `next(null, processedContent)` or `next(error)` -- example plugins: [karma-coffee-preprocessor], [karma-ng-html2js-preprocessor] -- use naming convention is `karma-*-preprocessor` -- user NPM keywords `karma-plugin`, `karma-preprocessor` +## Plugin structure -## Crazier stuff -Karma is assembled by Dependency Injection and a plugin is just an additional DI module (see [node-di] for more), that can be loaded by Karma. Therefore, it can ask for pretty much any Karma component and interact with it. There are a couple of plugins that do more interesting stuff like this, check out [karma-closure], [karma-intellij]. +Each plugin is essentially a service with its associated DI token. When user [activates a plugin][plugins] in their config, Karma looks for a corresponding DI token and instantiates a service linked to this DI token. +To declare a plugin one should define a DI token for the plugin and explain Karma how to instantiate it. A DI token consists of two parts: a plugin type and plugin's unique name. The former defines what a plugin can do, requirements to the service's API and when it is instantiated. The latter is a unique name, which a plugin user will use to activate a plugin. -[karma-jasmine]: https://github.com/karma-runner/karma-jasmine -[karma-mocha]: https://github.com/karma-runner/karma-mocha +It is totally valid for a plugin to define multiple services. This can be done by adding more keys to the object exported by the plugin. Common example of this would be `framework` + `reporter` plugins, which usually come together. -[karma-requirejs]: https://github.com/karma-runner/karma-requirejs -[karma-growl-reporter]: https://github.com/karma-runner/karma-growl-reporter -[karma-junit-reporter]: https://github.com/karma-runner/karma-junit-reporter -[karma-chrome-launcher]: https://github.com/karma-runner/karma-chrome-launcher -[karma-sauce-launcher]: https://github.com/karma-runner/karma-sauce-launcher -[karma-coffee-preprocessor]: https://github.com/karma-runner/karma-coffee-preprocessor -[karma-ng-html2js-preprocessor]: https://github.com/karma-runner/karma-ng-html2js-preprocessor -[karma-closure]: https://github.com/karma-runner/karma-closure -[karma-intellij]: https://github.com/karma-runner/karma-intellij -[node-di]: https://github.com/vojtajina/node-di -[karma-material-reporter]: https://github.com/ameerthehacker/karma-material-reporter +Let's make a very simple plugin, which prints "Hello, world!" when instantiated. We'll use a `framework` type as it is instantiated early in the Karma lifecycle and does not have any requirements to its API. Let's call our plugin "hello", so its unique name will be `hello`. Joining these two parts we get a DI token for our plugin `framework:hello`. Let's declare it. + +```js +// hello-plugin.js + +// A factory function for our plugin, it will be called, when Karma needs to +// instantiate a plugin. Normally it should return an instance of the service +// conforming to the API requirements of the plugin type (more on that below), +// but for our simple example we don't need any service and just print +// a message when function is called. +function helloFrameworkFactory() { + console.log('Hello, world!') +} + +module.exports = { + // Declare the plugin, so Karma knows that it exists. + // 'factory' tells Karma that it should call `helloFrameworkFactory` + // function and use whatever it returns as a service for the DI token + // `framework:hello`. + 'framework:hello': ['factory', helloFrameworkFactory] +}; +``` + +```js +// karma.conf.js + +module.exports = (config) => { + config.set({ + plugins: [ + require('./hello-plugin') + ], + // Activate our plugin by specifying its unique name in the + // corresponding configuration key. + frameworks: ['hello'] + }) +} +``` + +## Injecting dependencies + +In "Dependency injection" section we discussed that it is possible to inject any Karma services into a plugin and interact with them. This can be done by setting an `$inject` property on the plugin's factory function to an array of DI tokens plugin wishes to interact with. Karma will pick up this property and pass requested services to the factory functions as parameters. + +Let's make the `hello` framework a bit more useful and make it add `hello.js` file to the `files` array. This way users of the plugin can, for example, access a function defined in `hello.js` from their tests. + +```js +// hello-plugin.js + +// Add parameters to the function to receive requested services. +function helloFrameworkFactory(config) { + config.files.unshift({ + pattern: __dirname + '/hello.js', + included: true, + served: true, + watched: false + }) +} + +// Declare DI tokens plugin wants to inject. +helloFrameworkFactory.$inject = ['config'] + +module.exports = { + 'framework:hello': ['factory', helloFrameworkFactory] +}; +``` + +The Karma config is unchanged and is omitted for brevity. See above example for the plugin usage. + +Note: Currently, Karma uses [node-di] library as a DI implementation. The library is more powerful than what's documented above, however, the DI implementation may change in the future, so we recommend not to rely on the node-di implementation details. + +## Plugin types -## Karma Framework API +This section outlines API requirements and conventions for different plugin types. There also links to some plugins, which you can use for inspiration. -Karma Framework connects existing testing libraries to Karma's API, so that their -results can be displayed in a browser and sent back to the server. +### Frameworks + +- example plugins: [karma-jasmine], [karma-mocha], [karma-requirejs] +- use naming convention is `karma-*` +- use NPM keywords `karma-plugin`, `karma-framework`. + +A framework connects existing testing libraries to Karma's API, so that their results can be displayed in a browser and sent back to the server. Karma frameworks _must_ implement a `window.__karma__.start` method that Karma will call to start test execution. This function is called with an object that has methods @@ -89,3 +137,51 @@ statuses. The method takes an object of the form: skipped: Boolean // skipped / ran } ``` + +### Reporters + +- example plugins: [karma-growl-reporter], [karma-junit-reporter], [karma-material-reporter] +- use naming convention is `karma-*-reporter` +- use NPM keywords `karma-plugin`, `karma-reporter` + +### Launchers + +- example plugins: [karma-chrome-launcher], [karma-sauce-launcher] +- use naming convention is `karma-*-launcher` +- use NPM keywords `karma-plugin`, `karma-launcher` + +### Preprocessors + +- example plugins: [karma-coffee-preprocessor], [karma-ng-html2js-preprocessor] +- use naming convention is `karma-*-preprocessor` +- user NPM keywords `karma-plugin`, `karma-preprocessor` + +A preprocessor is a function that accepts three arguments (`content`, `file`, and `next`), mutates the content in some way, and passes it on to the next preprocessor. + +- arguments passed to preprocessor plugins: + - **`content`** of the file being processed + - **`file`** object describing the file being processed + - **path:** the current file, mutable file path. e. g. `some/file.coffee` -> `some/file.coffee.js` _This path is mutable and may not actually exist._ + - **originalPath:** the original, unmutated path + - **encodings:** A mutable, keyed object where the keys are a valid encoding type ('gzip', 'compress', 'br', etc.) and the values are the encoded content. Encoded content should be stored here and not resolved using `next(null, encodedContent)` + - **type:** determines how to include a file, when serving + - **`next`** function to be called when preprocessing is complete, should be called as `next(null, processedContent)` or `next(error)` + +### Crazier stuff + +As Karma is assembled by dependency injection, a plugin can ask for pretty much any Karma component and interact with it. There are a couple of plugins that do more interesting stuff like this, check out [karma-closure], [karma-intellij]. + +[karma-jasmine]: https://github.com/karma-runner/karma-jasmine +[karma-mocha]: https://github.com/karma-runner/karma-mocha +[karma-requirejs]: https://github.com/karma-runner/karma-requirejs +[karma-growl-reporter]: https://github.com/karma-runner/karma-growl-reporter +[karma-junit-reporter]: https://github.com/karma-runner/karma-junit-reporter +[karma-chrome-launcher]: https://github.com/karma-runner/karma-chrome-launcher +[karma-sauce-launcher]: https://github.com/karma-runner/karma-sauce-launcher +[karma-coffee-preprocessor]: https://github.com/karma-runner/karma-coffee-preprocessor +[karma-ng-html2js-preprocessor]: https://github.com/karma-runner/karma-ng-html2js-preprocessor +[karma-closure]: https://github.com/karma-runner/karma-closure +[karma-intellij]: https://github.com/karma-runner/karma-intellij +[node-di]: https://github.com/vojtajina/node-di +[karma-material-reporter]: https://github.com/ameerthehacker/karma-material-reporter +[plugins]: ../config/plugins.html diff --git a/gruntfile.js b/gruntfile.js deleted file mode 100644 index 40da12beb..000000000 --- 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/cli.js b/lib/cli.js index 0aac17e9a..6e7fdac4f 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -153,7 +153,7 @@ function describeRoot () { .command('stop [configFile]', 'Stop the server.', describeStop) .command('completion', 'Shell completion for karma.', describeCompletion) .demandCommand(1, 'Command not specified.') - .strict() + .strictCommands() .describe('help', 'Print usage and options.') .describe('version', 'Print current version.') } @@ -164,6 +164,7 @@ function describeInit (yargs) { 'INIT - Initialize a config file.\n\n' + 'Usage:\n' + ' $0 init [configFile]') + .strictCommands(false) .version(false) .positional('configFile', { describe: 'Name of the generated Karma configuration file', @@ -215,6 +216,7 @@ function describeRun (yargs) { 'RUN - Run the tests (requires running server).\n\n' + 'Usage:\n' + ' $0 run [configFile] [-- ]') + .strictCommands(false) .version(false) .positional('configFile', { describe: 'Path to the Karma configuration file', @@ -247,6 +249,7 @@ function describeStop (yargs) { 'STOP - Stop the server (requires running server).\n\n' + 'Usage:\n' + ' $0 stop [configFile]') + .strictCommands(false) .version(false) .positional('configFile', { describe: 'Path to the Karma configuration file', diff --git a/lib/config.js b/lib/config.js index 1ea49b85c..1192afbac 100644 --- a/lib/config.js +++ b/lib/config.js @@ -27,7 +27,7 @@ try { } catch (e) {} try { - require('ts-node').register() + require('ts-node') TYPE_SCRIPT_AVAILABLE = true } catch (e) {} @@ -226,7 +226,7 @@ function normalizeConfig (config, configFilePath) { ? [preprocessors[pattern]] : preprocessors[pattern] }) - // define custom launchers/preprocessors/reporters - create an inlined plugin + // define custom launchers/preprocessors/reporters - create a new plugin const module = Object.create(null) let hasSomeInlinedPlugin = false const types = ['launcher', 'preprocessor', 'reporter'] @@ -351,17 +351,37 @@ const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' + ' });\n' + ' };\n' -function parseConfig (configFilePath, cliOptions) { +function parseConfig (configFilePath, cliOptions, parseOptions) { + function fail () { + log.error(...arguments) + if (parseOptions && parseOptions.throwErrors === true) { + const errorMessage = Array.from(arguments).join(' ') + throw new Error(errorMessage) + } else { + const warningMessage = + 'The `parseConfig()` function historically called `process.exit(1)`' + + ' when it failed. This behavior is now deprecated and function will' + + ' throw an error in the next major release. To suppress this warning' + + ' pass `throwErrors: true` as a third argument to opt-in into the new' + + ' behavior and adjust your code to respond to the exception' + + ' accordingly.' + + ' Example: `parseConfig(path, cliOptions, { throwErrors: true })`' + log.warn(warningMessage) + process.exit(1) + } + } + let configModule if (configFilePath) { try { + if (path.extname(configFilePath) === '.ts' && TYPE_SCRIPT_AVAILABLE) { + require('ts-node').register() + } configModule = require(configFilePath) if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') { configModule = configModule.default } } catch (e) { - log.error('Error in config file!\n ' + e.stack || e) - const extension = path.extname(configFilePath) if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) { log.error('You need to install CoffeeScript.\n npm install coffeescript --save-dev') @@ -370,11 +390,10 @@ function parseConfig (configFilePath, cliOptions) { } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) { log.error('You need to install TypeScript.\n npm install typescript ts-node --save-dev') } - return process.exit(1) + return fail('Error in config file!\n ' + e.stack || e) } if (!helper.isFunction(configModule)) { - log.error('Config file must export a function!\n' + CONFIG_SYNTAX_HELP) - return process.exit(1) + return fail('Config file must export a function!\n' + CONFIG_SYNTAX_HELP) } } else { configModule = () => {} // if no config file path is passed, we define a dummy config module. @@ -395,8 +414,7 @@ function parseConfig (configFilePath, cliOptions) { try { configModule(config) } catch (e) { - log.error('Error in config file!\n', e) - return process.exit(1) + return fail('Error in config file!\n', e) } // merge the config from config file and cliOptions (precedence) diff --git a/lib/launchers/process.js b/lib/launchers/process.js index 072c15b81..a7aa101ce 100644 --- a/lib/launchers/process.js +++ b/lib/launchers/process.js @@ -93,6 +93,7 @@ function ProcessLauncher (spawn, tempDir, timer, processKillTimeout) { } else { errorOutput += err.toString() } + self._onProcessExit(-1, null, errorOutput) }) self._process.stderr.on('data', function (errBuff) { @@ -101,6 +102,10 @@ function ProcessLauncher (spawn, tempDir, timer, processKillTimeout) { } this._onProcessExit = function (code, signal, errorOutput) { + if (!self._process) { + // Both exit and error events trigger _onProcessExit(), but we only need one cleanup. + return + } log.debug(`Process ${self.name} exited with code ${code} and signal ${signal}`) let error = null diff --git a/lib/middleware/karma.js b/lib/middleware/karma.js index 3a34b4e58..37e9a46c8 100644 --- a/lib/middleware/karma.js +++ b/lib/middleware/karma.js @@ -222,10 +222,10 @@ function createKarmaMiddleware ( }) : [] return data - .replace('%SCRIPTS%', scriptTags.join('\n')) + .replace('%SCRIPTS%', () => scriptTags.join('\n')) .replace('%CLIENT_CONFIG%', 'window.__karma__.config = ' + JSON.stringify(client) + ';\n') - .replace('%SCRIPT_URL_ARRAY%', 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n') - .replace('%MAPPINGS%', 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n') + .replace('%SCRIPT_URL_ARRAY%', () => 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n') + .replace('%MAPPINGS%', () => 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n') .replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url)) }) }) diff --git a/lib/plugin.js b/lib/plugin.js index 2e0ad5a24..8805a288d 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -32,14 +32,23 @@ function resolve (plugins, emitter) { return } const pluginDirectory = path.normalize(path.join(__dirname, '/../..')) - const regexp = new RegExp(`^${plugin.replace('*', '.*')}`) + const regexp = new RegExp(`^${plugin.replace(/\*/g, '.*').replace(/\//g, '[/\\\\]')}`) log.debug(`Loading ${plugin} from ${pluginDirectory}`) fs.readdirSync(pluginDirectory) - .filter((pluginName) => !IGNORED_PACKAGES.includes(pluginName) && regexp.test(pluginName)) - .forEach((pluginName) => requirePlugin(`${pluginDirectory}/${pluginName}`)) + .map((e) => { + const modulePath = path.join(pluginDirectory, e) + if (e[0] === '@') { + return fs.readdirSync(modulePath).map((e) => path.join(modulePath, e)) + } + return modulePath + }) + .reduce((a, x) => a.concat(x), []) + .map((modulePath) => path.relative(pluginDirectory, modulePath)) + .filter((moduleName) => !IGNORED_PACKAGES.includes(moduleName) && regexp.test(moduleName)) + .forEach((pluginName) => requirePlugin(path.join(pluginDirectory, pluginName))) } else if (helper.isObject(plugin)) { - log.debug(`Loading inlined plugin (defining ${Object.keys(plugin).join(', ')}).`) + log.debug(`Loading inline plugin defining ${Object.keys(plugin).join(', ')}.`) modules.push(plugin) } else { log.error(`Invalid plugin ${plugin}`) @@ -50,4 +59,40 @@ function resolve (plugins, emitter) { return modules } -exports.resolve = resolve +/** + Create a function to handle errors in plugin loading. + @param {Object} injector, the dict of dependency injection objects. + @return function closed over injector, which reports errors. +*/ +function createInstantiatePlugin (injector) { + const emitter = injector.get('emitter') + // Cache to avoid report errors multiple times per plugin. + const pluginInstances = new Map() + return function instantiatePlugin (kind, name) { + if (pluginInstances.has(name)) { + return pluginInstances.get(name) + } + + let p + try { + p = injector.get(`${kind}:${name}`) + if (!p) { + log.error(`Failed to instantiate ${kind} ${name}`) + emitter.emit('load_error', kind, name) + } + } catch (e) { + if (e.message.includes(`No provider for "${kind}:${name}"`)) { + log.error(`Cannot load "${name}", it is not registered!\n Perhaps you are missing some plugin?`) + } else { + log.error(`Cannot load "${name}"!\n ` + e.stack) + } + emitter.emit('load_error', kind, name) + } + pluginInstances.set(name, p, `${kind}:${name}`) + return p + } +} + +createInstantiatePlugin.$inject = ['injector'] + +module.exports = { resolve, createInstantiatePlugin } diff --git a/lib/preprocessor.js b/lib/preprocessor.js index 4b56a3633..b6bf75695 100644 --- a/lib/preprocessor.js +++ b/lib/preprocessor.js @@ -70,36 +70,8 @@ async function runProcessors (preprocessors, file, content) { file.sha = CryptoUtils.sha1(content) } -function createPriorityPreprocessor (config = {}, preprocessorPriority, basePath, injector) { - const emitter = injector.get('emitter') - const instances = new Map() - - function instantiatePreprocessor (name) { - if (instances.has(name)) { - return instances.get(name) - } - - let p - try { - p = injector.get('preprocessor:' + name) - if (!p) { - log.error(`Failed to instantiate preprocessor ${name}`) - emitter.emit('load_error', 'preprocessor', name) - } - } catch (e) { - if (e.message.includes(`No provider for "preprocessor:${name}"`)) { - log.error(`Can not load "${name}", it is not registered!\n Perhaps you are missing some plugin?`) - } else { - log.error(`Can not load "${name}"!\n ` + e.stack) - } - emitter.emit('load_error', 'preprocessor', name) - } - - instances.set(name, p) - return p - } - _.union.apply(_, Object.values(config)).forEach(instantiatePreprocessor) - +function createPriorityPreprocessor (config = {}, preprocessorPriority, basePath, instantiatePlugin) { + _.union.apply(_, Object.values(config)).forEach((name) => instantiatePlugin('preprocessor', name)) return async function preprocess (file) { const buffer = await tryToRead(file.originalPath, log) let isBinary = file.isBinary @@ -121,7 +93,7 @@ function createPriorityPreprocessor (config = {}, preprocessorPriority, basePath .sort((a, b) => b[1] - a[1]) .map((duo) => duo[0]) .reduce((preProcs, name) => { - const p = instantiatePreprocessor(name) + const p = instantiatePlugin('preprocessor', name) if (!isBinary || (p && p.handleBinaryFiles)) { preProcs.push(p) @@ -135,5 +107,5 @@ function createPriorityPreprocessor (config = {}, preprocessorPriority, basePath } } -createPriorityPreprocessor.$inject = ['config.preprocessors', 'config.preprocessor_priority', 'config.basePath', 'injector'] +createPriorityPreprocessor.$inject = ['config.preprocessors', 'config.preprocessor_priority', 'config.basePath', 'instantiatePlugin'] exports.createPriorityPreprocessor = createPriorityPreprocessor diff --git a/lib/server.js b/lib/server.js index ff03553eb..a6ae81dab 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 @@ -61,7 +63,15 @@ class Server extends KarmaEventEmitter { this.loadErrors = [] - const config = cfg.parseConfig(cliOptions.configFile, cliOptions) + let config + try { + config = cfg.parseConfig(cliOptions.configFile, cliOptions, { throwErrors: true }) + } catch (parseConfigError) { + // TODO: change how `done` falls back to exit in next major version + // SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378 + (done || process.exit)(1) + return + } this.log.debug('Final config', util.inspect(config, false, /** depth **/ null)) @@ -74,6 +84,7 @@ class Server extends KarmaEventEmitter { watcher: ['value', watcher], launcher: ['factory', Launcher.factory], config: ['value', config], + instantiatePlugin: ['factory', plugin.createInstantiatePlugin], preprocess: ['factory', preprocessor.createPriorityPreprocessor], fileList: ['factory', FileList.factory], webServer: ['factory', createWebServer], @@ -82,6 +93,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 379818435..394066b7b 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 b92a4b0a0..ab7b36418 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "karma", - "version": "6.0.0", + "version": "6.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -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", @@ -4321,31 +4297,31 @@ } }, "engine.io": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.5.tgz", - "integrity": "sha512-Ri+whTNr2PKklxQkfbGjwEo+kCBUM4Qxk4wtLqLrhH+b1up2NFL9g9pjYWiCV/oazwB0rArnvF/ZmZN2ab5Hpg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.0.tgz", + "integrity": "sha512-vW7EAtn0HDQ4MtT5QbmCHF17TaYLONv2/JwdYsq9USPRZVM4zG7WB3k0Nc321z8EuSOlhGokrYlYx4176QhD0A==", "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", "cors": "~2.8.5", - "debug": "~4.1.0", + "debug": "~4.3.1", "engine.io-parser": "~4.0.0", - "ws": "^7.1.2" + "ws": "~7.4.2" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -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", @@ -12920,54 +12451,54 @@ } }, "socket.io": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.4.tgz", - "integrity": "sha512-Vj1jUoO75WGc9txWd311ZJJqS9Dr8QtNJJ7gk2r7dcM/yGe9sit7qOijQl3GAwhpBOz/W8CwkD7R6yob07nLbA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.0.tgz", + "integrity": "sha512-Aqg2dlRh6xSJvRYK31ksG65q4kmBOqU4g+1ukhPcoT6wNGYoIwSYPlCPuRwOO9pgLUajojGFztl6+V2opmKcww==", "requires": { "@types/cookie": "^0.4.0", "@types/cors": "^2.8.8", - "@types/node": "^14.14.7", + "@types/node": "^14.14.10", "accepts": "~1.3.4", "base64id": "~2.0.0", - "debug": "~4.1.0", - "engine.io": "~4.0.0", - "socket.io-adapter": "~2.0.3", - "socket.io-parser": "~4.0.1" + "debug": "~4.3.1", + "engine.io": "~4.1.0", + "socket.io-adapter": "~2.1.0", + "socket.io-parser": "~4.0.3" }, "dependencies": { "@types/node": { - "version": "14.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz", - "integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ==" + "version": "14.14.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, "socket.io-adapter": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", - "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", + "integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==" }, "socket.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz", - "integrity": "sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", "requires": { "@types/component-emitter": "^1.2.10", "component-emitter": "~1.3.0", - "debug": "~4.1.0" + "debug": "~4.3.1" }, "dependencies": { "component-emitter": { @@ -12976,17 +12507,17 @@ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -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", @@ -14306,9 +13812,9 @@ } }, "ws": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", - "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==" + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" }, "xmlbuilder": { "version": "12.0.0", diff --git a/package.json b/package.json index 9fdce2cad..5c2110653 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "Karma Bot ", "Maksim Ryzhikov ", "ukasz Usarz ", - "Christian Budde Christensen ", "semantic-release-bot ", + "Christian Budde Christensen ", "Wesley Cho ", "taichi ", "Liam Newman ", @@ -79,6 +79,7 @@ "Bryan Smith ", "Bulat Shakirzyanov ", "ChangZhuo Chen (陳昌倬) ", + "Chris Bottin ", "Cyrus Chan ", "DarthCharles ", "David Herges ", @@ -172,6 +173,7 @@ "Carl Goldberg ", "Chad Smith ", "Chang Wang ", + "Charles Suh ", "Chelsea Urquhart ", "Chris ", "Chris Chua ", @@ -257,6 +259,7 @@ "Kostiantyn Kahanskyi ", "Kris Kowal ", "Lenny Urbanowski ", + "Long Ho ", "LoveIsGrief ", "Lucas Theisen ", "Lukasz Zatorski ", @@ -291,6 +294,7 @@ "Nick Carter ", "Nick McCurdy ", "Nick Payne ", + "Nick Petruzzelli ", "Nick Williams ", "Nicolas Artman ", "Nicolas Ferrero ", @@ -326,6 +330,7 @@ "Remy Sharp ", "Ricardo Melo Joia ", "Rich Kuzsma ", + "Rich Trott ", "Richard Herrera ", "Roarke Gaskill ", "Rob Cherry ", @@ -388,6 +393,7 @@ "deepak1556 ", "dorey ", "grifball ", + "hdmr14 <58992133+hdmr14@users.noreply.github.com>", "hrgdavor ", "ianjobling ", "inf3rno ", @@ -404,6 +410,7 @@ "thetrevdev ", "thorn0 ", "toran billups ", + "xel23 ", "chalkerx@gmail.com>", "weiran.zsd@outlook.com>" ], @@ -426,7 +433,7 @@ "qjobs": "^1.2.0", "range-parser": "^1.2.1", "rimraf": "^3.0.2", - "socket.io": "^3.0.4", + "socket.io": "^3.1.0", "source-map": "^0.6.1", "tmp": "0.2.1", "ua-parser-js": "^0.7.23", @@ -449,9 +456,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", @@ -485,7 +489,7 @@ "engines": { "node": ">= 10" }, - "version": "6.0.0", + "version": "6.2.0", "license": "MIT", "husky": { "hooks": { @@ -496,7 +500,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", diff --git a/static/context.js b/static/context.js index 8cc67ebf6..417503165 100644 --- a/static/context.js +++ b/static/context.js @@ -214,9 +214,9 @@ function ContextKarma (callParentKarmaMethod) { contextWindow.onerror = function () { return self.error.apply(self, arguments) } - // DEV: We must defined a function since we don't want to pass the event object - contextWindow.onbeforeunload = function (e, b) { - callParentKarmaMethod('onbeforeunload', []) + + contextWindow.onbeforeunload = function () { + return self.error('Some of your tests did a full page reload!') } contextWindow.dump = function () { diff --git a/static/karma.js b/static/karma.js index 8c08d3fca..23b7c1ae2 100644 --- a/static/karma.js +++ b/static/karma.js @@ -15,7 +15,6 @@ var util = require('../common/util') function Karma (updater, socket, iframe, opener, navigator, location, document) { this.updater = updater var startEmitted = false - var karmaNavigating = false var self = this var queryParams = util.parseQueryParams(location.search) var browserId = queryParams.id || util.generateId('manual-') @@ -93,21 +92,21 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) var childWindow = null function navigateContextTo (url) { - karmaNavigating = true if (self.config.useIframe === false) { // run in new window if (self.config.runInParent === false) { // If there is a window already open, then close it // DEV: In some environments (e.g. Electron), we don't have setter access for location if (childWindow !== null && childWindow.closed !== true) { + // The onbeforeunload listener was added by context to catch + // unexpected navigations while running tests. + childWindow.onbeforeunload = undefined childWindow.close() } childWindow = opener(url) - karmaNavigating = false // run context on parent element (client_with_context) // using window.__karma__.scriptUrls to get the html element strings and load them dynamically } else if (url !== 'about:blank') { - karmaNavigating = false var loadScript = function (idx) { if (idx < window.__karma__.scriptUrls.length) { var parser = new DOMParser() @@ -138,15 +137,10 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) } // run in iframe } else { + // The onbeforeunload listener was added by the context to catch + // unexpected navigations while running tests. + iframe.contentWindow.onbeforeunload = undefined iframe.src = policy.createURL(url) - karmaNavigating = false - } - } - - this.onbeforeunload = function () { - if (!karmaNavigating) { - // TODO(vojta): show what test (with explanation about jasmine.UPDATE_INTERVAL) - self.error('Some of your tests did a full page reload!') } } @@ -201,7 +195,7 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) } socket.emit('karma_error', message) - self.updater.updateTestStatus(`karma_error ${message}`) + self.updater.updateTestStatus('karma_error ' + message) this.complete() return false } @@ -250,8 +244,9 @@ function Karma (updater, socket, iframe, opener, navigator, location, document) // A test could have incorrectly issued a navigate. Wait one turn // to ensure the error from an incorrect navigate is processed. - setTimeout(() => { - if (this.config.clearContext) { + var config = this.config + setTimeout(function () { + if (config.clearContext) { navigateContextTo('about:blank') } @@ -384,7 +379,7 @@ function StatusUpdater (socket, titleElement, bannerElement, browsersElement) { if (!titleElement || !bannerElement) { return } - titleElement.textContent = `Karma v ${VERSION} - ${connectionText}; test: ${testText}; ${pingText}` + titleElement.textContent = 'Karma v ' + VERSION + ' - ' + connectionText + '; test: ' + testText + '; ' + pingText bannerElement.className = connectionText === 'connected' ? 'online' : 'offline' } @@ -401,32 +396,32 @@ function StatusUpdater (socket, titleElement, bannerElement, browsersElement) { updateBanner() } - socket.on('connect', () => { + socket.on('connect', function () { updateConnectionStatus('connected') }) - socket.on('disconnect', () => { + socket.on('disconnect', function () { updateConnectionStatus('disconnected') }) - socket.on('reconnecting', (sec) => { - updateConnectionStatus(`reconnecting in ${sec} seconds`) + socket.on('reconnecting', function (sec) { + updateConnectionStatus('reconnecting in ' + sec + ' seconds') }) - socket.on('reconnect', () => { + socket.on('reconnect', function () { updateConnectionStatus('reconnected') }) - socket.on('reconnect_failed', () => { + socket.on('reconnect_failed', function () { updateConnectionStatus('reconnect_failed') }) socket.on('info', updateBrowsersInfo) - socket.on('disconnect', () => { + socket.on('disconnect', function () { updateBrowsersInfo([]) }) - socket.on('ping', () => { + socket.on('ping', function () { updatePingStatus('ping...') }) - socket.on('pong', (latency) => { - updatePingStatus(`ping ${latency}ms`) + socket.on('pong', function (latency) { + updatePingStatus('ping ' + latency + 'ms') }) return { updateTestStatus: updateTestStatus } diff --git a/test/client/karma.conf.js b/test/client/karma.conf.js index 0b1281d78..838e3b78d 100644 --- a/test/client/karma.conf.js +++ b/test/client/karma.conf.js @@ -14,35 +14,20 @@ const launchers = { browser: 'firefox', os: 'Windows', os_version: '10' + }, + bs_safari: { + base: 'BrowserStack', + browser: 'Safari', + os: 'OS X', + os_version: 'Big Sur' + }, + bs_ie: { + base: 'BrowserStack', + browser: 'IE', + browser_version: '11.0', + os: 'Windows', + os_version: '10' } - // bs_safari: { - // base: 'BrowserStack', - // browser: 'safari', - // browser_version: '9.0', - // os_version: 'El Capitan', - // os: 'OS X' - // }, - // bs_ie_11: { - // base: 'BrowserStack', - // browser: 'ie', - // browser_version: '11.0', - // os: 'Windows', - // os_version: '10' - // }, - // bs_ie_10: { - // base: 'BrowserStack', - // browser: 'ie', - // browser_version: '10.0', - // os: 'Windows', - // os_version: '8' - // }, - // bs_ie_9: { - // base: 'BrowserStack', - // browser: 'ie', - // browser_version: '9.0', - // os: 'Windows', - // os_version: '7' - // } } // Verify the install. This will run async but that's ok we'll see the log. diff --git a/test/client/karma.spec.js b/test/client/karma.spec.js index b1c50a00a..3e7af73d8 100644 --- a/test/client/karma.spec.js +++ b/test/client/karma.spec.js @@ -17,12 +17,12 @@ describe('Karma', function () { beforeEach(function () { mockTestStatus = '' updater = { - updateTestStatus: (s) => { + updateTestStatus: function (s) { mockTestStatus = s } } socket = new MockSocket() - iframe = {} + iframe = { contentWindow: {} } windowNavigator = {} windowLocation = { search: '' } windowStub = sinon.stub().returns({}) @@ -454,7 +454,7 @@ describe('Karma', function () { clock.tick(500) ck.complete() - setTimeout(() => { + setTimeout(function () { assert(windowLocation.href === 'http://return.com') done() }, 5) diff --git a/test/client/stringify.spec.js b/test/client/stringify.spec.js index 921ec3366..a3c744ca5 100644 --- a/test/client/stringify.spec.js +++ b/test/client/stringify.spec.js @@ -49,7 +49,9 @@ describe('stringify', function () { if (window.Proxy) { it('should serialize proxied functions', function () { var defProxy = new Proxy(function (d, e, f) { return 'whatever' }, {}) - assert.deepStrictEqual(stringify(defProxy), 'function () { ... }') + // In Safari stringified Proxy object has ProxyObject as a name, but + // in other browsers it does not. + assert.deepStrictEqual(/^function (ProxyObject)?\(\) { ... }$/.test(stringify(defProxy)), true) }) } diff --git a/test/e2e/cli.feature b/test/e2e/cli.feature index 476d29a8e..e4f235985 100644 --- a/test/e2e/cli.feature +++ b/test/e2e/cli.feature @@ -55,52 +55,24 @@ Feature: CLI --help Print usage and options. [boolean] --version Print current version. [boolean] - Unknown argument: strat + Unknown command: strat """ - Scenario: Error when option is unknown - When I execute Karma with arguments: "start --invalid-option" - Then the stderr is exactly: + Scenario: No error when unknown option and argument are passed in + Given a configuration with: """ - Karma - Spectacular Test Runner for JavaScript. - - START - Start the server / do a single run. - - Usage: - karma start [configFile] - - Positionals: - configFile Path to the Karma configuration file [string] - - Options: - --help Print usage and options. [boolean] - --port Port where the server is running. - --auto-watch Auto watch source files and run on change. - --detached Detach the server. - --no-auto-watch Do not watch source files. - --log-level Level - of logging. - --colors Use colors when reporting and printing logs. - --no-colors Do not use colors when reporting or printing - logs. - --reporters List of reporters (available: dots, progress, - junit, growl, coverage). - --browsers List of browsers to start (eg. --browsers - Chrome,ChromeCanary,Firefox). - --capture-timeout Kill browser if does not capture in - given time [ms]. - --single-run Run the test when browsers captured and exit. - --no-single-run Disable single-run. - --report-slower-than Report tests that are slower than - given time [ms]. - --fail-on-empty-test-suite Fail on empty test suite. - --no-fail-on-empty-test-suite Do not fail on empty test suite. - --fail-on-failing-test-suite Fail on failing test suite. - --no-fail-on-failing-test-suite Do not fail on failing test suite. - --format-error A path to a file that exports the format - function. [string] - - Unknown arguments: invalid-option, invalidOption + files = ['basic/plus.js', 'basic/test.js']; + browsers = ['ChromeHeadlessNoSandbox']; + plugins = [ + 'karma-jasmine', + 'karma-chrome-launcher' + ]; + """ + When I execute Karma with arguments: "start sandbox/karma.conf.js unknown-argument --unknown-option" + Then it passes with: + """ + .. + Chrome Headless """ Scenario: Init command help diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 000000000..58a295fd3 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,3 @@ +--reporter dot +--ui bdd +test/unit/mocha-globals.js diff --git a/test/unit/config.spec.js b/test/unit/config.spec.js index 169c418af..d115a2b6e 100644 --- a/test/unit/config.spec.js +++ b/test/unit/config.spec.js @@ -46,6 +46,8 @@ describe('config', () => { '/conf/invalid.js': () => { throw new SyntaxError('Unexpected token =') }, + '/conf/export-not-function.js': 'not-a-function', + // '/conf/export-null.js': null, // Same as `/conf/not-exist.js`? '/conf/exclude.js': wrapCfg({ exclude: ['one.js', 'sub/two.js'] }), '/conf/absolute.js': wrapCfg({ files: ['http://some.com', 'https://more.org/file.js'] }), '/conf/both.js': wrapCfg({ files: ['one.js', 'two.js'], exclude: ['third.js'] }), @@ -57,6 +59,7 @@ describe('config', () => { m = loadFile(path.join(__dirname, '/../../lib/config.js'), mocks, { global: {}, process: mocks.process, + Error: Error, // Without this, chai's `.throw()` assertion won't correctly check against constructors. require (path) { if (mockConfigs[path]) { return mockConfigs[path] @@ -123,7 +126,20 @@ describe('config', () => { expect(mocks.process.exit).to.have.been.calledWith(1) }) - it('should throw and log error if invalid file', () => { + it('should log error and throw if file does not exist AND throwErrors is true', () => { + function parseConfig () { + e.parseConfig('/conf/not-exist.js', {}, { throwErrors: true }) + } + + expect(parseConfig).to.throw(Error, 'Error in config file!\n Error: Cannot find module \'/conf/not-exist.js\'') + expect(logSpy).to.have.been.called + const event = logSpy.lastCall.args + expect(event.toString().split('\n').slice(0, 2)).to.be.deep.equal( + ['Error in config file!', ' Error: Cannot find module \'/conf/not-exist.js\'']) + expect(mocks.process.exit).not.to.have.been.called + }) + + it('should log an error and exit if invalid file', () => { e.parseConfig('/conf/invalid.js', {}) expect(logSpy).to.have.been.called @@ -133,6 +149,32 @@ describe('config', () => { expect(mocks.process.exit).to.have.been.calledWith(1) }) + it('should log an error and throw if invalid file AND throwErrors is true', () => { + function parseConfig () { + e.parseConfig('/conf/invalid.js', {}, { throwErrors: true }) + } + + expect(parseConfig).to.throw(Error, 'Error in config file!\n SyntaxError: Unexpected token =') + expect(logSpy).to.have.been.called + const event = logSpy.lastCall.args + expect(event[0]).to.eql('Error in config file!\n') + expect(event[1].message).to.eql('Unexpected token =') + expect(mocks.process.exit).not.to.have.been.called + }) + + it('should log error and throw if file does not export a function AND throwErrors is true', () => { + function parseConfig () { + e.parseConfig('/conf/export-not-function.js', {}, { throwErrors: true }) + } + + expect(parseConfig).to.throw(Error, 'Config file must export a function!\n') + expect(logSpy).to.have.been.called + const event = logSpy.lastCall.args + expect(event.toString().split('\n').slice(0, 1)).to.be.deep.equal( + ['Config file must export a function!']) + expect(mocks.process.exit).not.to.have.been.called + }) + it('should override config with given cli options', () => { const config = e.parseConfig('/home/config4.js', { port: 456, autoWatch: false }) diff --git a/test/unit/launchers/process.spec.js b/test/unit/launchers/process.spec.js index 6dfc29b40..489d7ecdf 100644 --- a/test/unit/launchers/process.spec.js +++ b/test/unit/launchers/process.spec.js @@ -7,18 +7,25 @@ const CaptureTimeoutLauncher = require('../../../lib/launchers/capture_timeout') const ProcessLauncher = require('../../../lib/launchers/process') const EventEmitter = require('../../../lib/events').EventEmitter const createMockTimer = require('../mocks/timer') +const logger = require('../../../lib/logger') describe('launchers/process.js', () => { let emitter let mockSpawn let mockTempDir let launcher + let logErrorSpy + let logDebugSpy const BROWSER_PATH = path.normalize('/usr/bin/browser') beforeEach(() => { emitter = new EventEmitter() launcher = new BaseLauncher('fake-id', emitter) + launcher.name = 'fake-name' + launcher.ENV_CMD = 'fake-ENV-CMD' + logErrorSpy = sinon.spy(logger.create('launcher'), 'error') + logDebugSpy = sinon.spy(logger.create('launcher'), 'debug') mockSpawn = sinon.spy(function (cmd, args) { const process = new EventEmitter() @@ -74,7 +81,7 @@ describe('launchers/process.js', () => { }) describe('with RetryLauncher', () => { - it('should handle spawn ENOENT error and not even retry', (done) => { + function assertSpawnError ({ errorCode, emitExit, expectedError }, done) { ProcessLauncher.call(launcher, mockSpawn, mockTempDir) RetryLauncher.call(launcher, 2) launcher._getCommand = () => BROWSER_PATH @@ -83,35 +90,56 @@ describe('launchers/process.js', () => { emitter.on('browser_process_failure', failureSpy) launcher.start('http://host:9876/') - mockSpawn._processes[0].emit('error', { code: 'ENOENT' }) - mockSpawn._processes[0].emit('exit', 1) + mockSpawn._processes[0].emit('error', { code: errorCode }) + if (emitExit) { + mockSpawn._processes[0].emit('exit', 1) + } mockTempDir.remove.callArg(1) _.defer(() => { expect(launcher.state).to.equal(launcher.STATE_FINISHED) expect(failureSpy).to.have.been.called + expect(logDebugSpy).to.have.been.callCount(5) + expect(logDebugSpy.getCall(0)).to.have.been.calledWithExactly('null -> BEING_CAPTURED') + expect(logDebugSpy.getCall(1)).to.have.been.calledWithExactly(`${BROWSER_PATH} http://host:9876/?id=fake-id`) + expect(logDebugSpy.getCall(2)).to.have.been.calledWithExactly('Process fake-name exited with code -1 and signal null') + expect(logDebugSpy.getCall(3)).to.have.been.calledWithExactly('fake-name failed (cannot start). Not restarting.') + expect(logDebugSpy.getCall(4)).to.have.been.calledWithExactly('BEING_CAPTURED -> FINISHED') + expect(logErrorSpy).to.have.been.calledWith(expectedError) done() }) + } + + it('should handle spawn ENOENT error and not even retry', (done) => { + assertSpawnError({ + errorCode: 'ENOENT', + emitExit: true, + expectedError: `Cannot start fake-name\n\tCan not find the binary ${BROWSER_PATH}\n\tPlease set env variable fake-ENV-CMD` + }, done) }) it('should handle spawn EACCES error and not even retry', (done) => { - ProcessLauncher.call(launcher, mockSpawn, mockTempDir) - RetryLauncher.call(launcher, 2) - launcher._getCommand = () => BROWSER_PATH - - const failureSpy = sinon.spy() - emitter.on('browser_process_failure', failureSpy) + assertSpawnError({ + errorCode: 'EACCES', + emitExit: true, + expectedError: `Cannot start fake-name\n\tPermission denied accessing the binary ${BROWSER_PATH}\n\tMaybe it's a directory?` + }, done) + }) - launcher.start('http://host:9876/') - mockSpawn._processes[0].emit('error', { code: 'EACCES' }) - mockSpawn._processes[0].emit('exit', 1) - mockTempDir.remove.callArg(1) + it('should handle spawn ENOENT error and report the error when exit event is not emitted', (done) => { + assertSpawnError({ + errorCode: 'ENOENT', + emitExit: false, + expectedError: `Cannot start fake-name\n\tCan not find the binary ${BROWSER_PATH}\n\tPlease set env variable fake-ENV-CMD` + }, done) + }) - _.defer(() => { - expect(launcher.state).to.equal(launcher.STATE_FINISHED) - expect(failureSpy).to.have.been.called - done() - }) + it('should handle spawn EACCES error and report the error when exit event is not emitted', (done) => { + assertSpawnError({ + errorCode: 'EACCES', + emitExit: false, + expectedError: `Cannot start fake-name\n\tPermission denied accessing the binary ${BROWSER_PATH}\n\tMaybe it's a directory?` + }, done) }) }) diff --git a/test/unit/middleware/karma.spec.js b/test/unit/middleware/karma.spec.js index 8a968262c..b6a0d5d59 100644 --- a/test/unit/middleware/karma.spec.js +++ b/test/unit/middleware/karma.spec.js @@ -28,6 +28,7 @@ describe('middleware.karma', () => { karma: { static: { 'client.html': mocks.fs.file(0, 'CLIENT HTML\n%X_UA_COMPATIBLE%%X_UA_COMPATIBLE_URL%'), + 'client_with_context.html': mocks.fs.file(0, 'CLIENT_WITH_CONTEXT\n%SCRIPT_URL_ARRAY%'), 'context.html': mocks.fs.file(0, 'CONTEXT\n%SCRIPTS%'), 'debug.html': mocks.fs.file(0, 'DEBUG\n%SCRIPTS%\n%X_UA_COMPATIBLE%'), 'karma.js': mocks.fs.file(0, 'root: %KARMA_URL_ROOT%, proxy: %KARMA_PROXY_PATH%, v: %KARMA_VERSION%') @@ -214,6 +215,21 @@ describe('middleware.karma', () => { callHandlerWith('/__karma__/context.html') }) + it('should serve context.html without using special patterns when replacing script tags', (done) => { + includedFiles([ + new MockFile('/.yarn/$$virtual/first.js', 'sha123'), + new MockFile('/.yarn/$$virtual/second.dart', 'sha456') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CONTEXT\n\n') + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + it('should serve context.html with replaced link tags', (done) => { includedFiles([ new MockFile('/first.css', 'sha007'), @@ -373,6 +389,20 @@ describe('middleware.karma', () => { callHandlerWith('/__karma__/context.html') }) + it('should inline mappings without using special patterns', (done) => { + fsMock._touchFile('/karma/static/context.html', 0, '%MAPPINGS%') + servedFiles([ + new MockFile('/.yarn/$$virtual/abc/a.js', 'sha_a') + ]) + + response.once('end', () => { + expect(response).to.beServedAs(200, "window.__karma__.files = {\n '/__proxy__/__karma__/absolute/.yarn/$$virtual/abc/a.js': 'sha_a'\n};\n") + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + it('should escape quotes in mappings with all served files', (done) => { fsMock._touchFile('/karma/static/context.html', 0, '%MAPPINGS%') servedFiles([ @@ -490,4 +520,19 @@ describe('middleware.karma', () => { callHandlerWith('/__karma__/debug.html') }) + + it('should serve client_with_context.html without using special patterns when replacing script urls', (done) => { + includedFiles([ + new MockFile('/.yarn/$$virtual/first.js', 'sha123'), + new MockFile('/.yarn/$$virtual/second.dart', 'sha456') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CLIENT_WITH_CONTEXT\nwindow.__karma__.scriptUrls = ["\\\\x3Cscript type=\\"text/javascript\\" src=\\"/__proxy__/__karma__/absolute/.yarn/$$virtual/first.js\\" crossorigin=\\"anonymous\\"\\\\x3E\\\\x3C/script\\\\x3E","\\\\x3Cscript type=\\"text/javascript\\" src=\\"/__proxy__/__karma__/absolute/.yarn/$$virtual/second.dart\\" crossorigin=\\"anonymous\\"\\\\x3E\\\\x3C/script\\\\x3E"];\n') + done() + }) + + callHandlerWith('/__karma__/client_with_context.html') + }) }) diff --git a/test/unit/mocha-globals.js b/test/unit/mocha-globals.js index 8afdd587b..6659714ee 100644 --- a/test/unit/mocha-globals.js +++ b/test/unit/mocha-globals.js @@ -1,6 +1,7 @@ const sinon = require('sinon') const chai = require('chai') const logger = require('../../lib/logger') +const recording = require('log4js/lib/appenders/recording') // publish globals that all specs can use global.expect = chai.expect @@ -15,13 +16,14 @@ chai.use(require('chai-subset')) beforeEach(() => { global.sinon = sinon.createSandbox() - // set logger to log INFO, but do not append to console - // so that we can assert logs by logger.on('info', ...) - logger.setup('INFO', false, []) + // Use https://log4js-node.github.io/log4js-node/recording.html to verify logs + const vcr = { vcr: { type: 'recording' } } + logger.setup('INFO', false, vcr) }) afterEach(() => { global.sinon.restore() + recording.erase() }) // TODO(vojta): move to helpers or something diff --git a/test/unit/plugin.spec.js b/test/unit/plugin.spec.js new file mode 100644 index 000000000..3ec9824d1 --- /dev/null +++ b/test/unit/plugin.spec.js @@ -0,0 +1,61 @@ +'use strict' + +const createInstantiatePlugin = require('../../lib/plugin').createInstantiatePlugin + +describe('plugin', () => { + describe('createInstantiatePlugin', () => { + it('creates the instantiatePlugin function', () => { + const fakeGet = sinon.stub() + const fakeInjector = { get: fakeGet } + + expect(typeof createInstantiatePlugin(fakeInjector)).to.be.equal('function') + expect(fakeGet).to.have.been.calledWith('emitter') + }) + + it('creates the instantiatePlugin function', () => { + const fakes = { + emitter: { emit: sinon.stub() } + } + const fakeInjector = { get: (id) => fakes[id] } + + const instantiatePlugin = createInstantiatePlugin(fakeInjector) + expect(typeof instantiatePlugin('kind', 'name')).to.be.equal('undefined') + expect(fakes.emitter.emit).to.have.been.calledWith('load_error', 'kind', 'name') + }) + + it('caches plugins', () => { + const fakes = { + emitter: { emit: sinon.stub() }, + 'kind:name': { my: 'plugin' } + } + const fakeInjector = { + get: (id) => { + return fakes[id] + } + } + + const instantiatePlugin = createInstantiatePlugin(fakeInjector) + expect(instantiatePlugin('kind', 'name')).to.be.equal(fakes['kind:name']) + fakeInjector.get = (id) => { throw new Error('failed to cache') } + expect(instantiatePlugin('kind', 'name')).to.be.equal(fakes['kind:name']) + }) + + it('errors if the injector errors', () => { + const fakes = { + emitter: { emit: sinon.stub() } + } + const fakeInjector = { + get: (id) => { + if (id in fakes) { + return fakes[id] + } + throw new Error('fail') + } + } + + const instantiatePlugin = createInstantiatePlugin(fakeInjector) + expect(typeof instantiatePlugin('unknown', 'name')).to.be.equal('undefined') + expect(fakes.emitter.emit).to.have.been.calledWith('load_error', 'unknown', 'name') + }) + }) +}) diff --git a/test/unit/preprocessor.spec.js b/test/unit/preprocessor.spec.js index 2b5fefbc2..5f0b7860d 100644 --- a/test/unit/preprocessor.spec.js +++ b/test/unit/preprocessor.spec.js @@ -1,18 +1,19 @@ 'use strict' const mocks = require('mocks') -const di = require('di') const path = require('path') -const events = require('../../lib/events') - describe('preprocessor', () => { let m let mockFs - let emitterSetting // mimic first few bytes of a pdf file const binarydata = Buffer.from([0x25, 0x50, 0x44, 0x66, 0x46, 0x00]) + // Each test will define a spy; the fakeInstatiatePlugin will return it. + let fakePreprocessor + + const simpleFakeInstantiatePlugin = () => { return fakePreprocessor } + beforeEach(() => { mockFs = mocks.fs.create({ some: { @@ -32,22 +33,16 @@ describe('preprocessor', () => { 'graceful-fs': mockFs, minimatch: require('minimatch') } - emitterSetting = { emitter: ['value', new events.EventEmitter()] } m = mocks.loadFile(path.join(__dirname, '/../../lib/preprocessor.js'), mocks_) }) it('should preprocess matching file', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { file.path = file.path + '-preprocessed' done(null, 'new-content') }) - const injector = new di.Injector([{ - 'preprocessor:fake': [ - 'factory', function () { return fakePreprocessor } - ] - }, emitterSetting]) - const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } @@ -58,15 +53,12 @@ describe('preprocessor', () => { }) it('should match directories starting with a dot', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { file.path = file.path + '-preprocessed' done(null, 'new-content') }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/.dir/a.js', path: 'path' } @@ -77,15 +69,12 @@ describe('preprocessor', () => { }) it('should get content if preprocessor is an async function or return Promise with content', async () => { - const fakePreprocessor = sinon.spy(async (content, file, done) => { + fakePreprocessor = sinon.spy(async (content, file, done) => { file.path = file.path + '-preprocessed' return 'new-content' }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/.dir/a.js', path: 'path' } @@ -96,15 +85,12 @@ describe('preprocessor', () => { }) it('should get content if preprocessor is an async function still calling done()', async () => { - const fakePreprocessor = sinon.spy(async (content, file, done) => { + fakePreprocessor = sinon.spy(async (content, file, done) => { file.path = file.path + '-preprocessed' done(null, 'new-content') }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/.dir/a.js', path: 'path' } @@ -115,16 +101,13 @@ describe('preprocessor', () => { }) it('should check patterns after creation when invoked', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { file.path = file.path + '-preprocessed' done(null, 'new-content') }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) const config = { '**/*.txt': ['fake'] } - const pp = m.createPriorityPreprocessor(config, {}, null, injector) + const pp = m.createPriorityPreprocessor(config, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } @@ -137,14 +120,11 @@ describe('preprocessor', () => { }) it('should ignore not matching file', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, '') }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/a.txt', path: 'path' } @@ -153,34 +133,33 @@ describe('preprocessor', () => { }) it('should apply all preprocessors', async () => { - const fakePreprocessor1 = sinon.spy((content, file, done) => { - file.path = file.path + '-p1' - done(null, content + '-c1') - }) - - const fakePreprocessor2 = sinon.spy((content, file, done) => { - file.path = file.path + '-p2' - done(content + '-c2') - }) - - const injector = new di.Injector([{ - 'preprocessor:fake1': ['factory', function () { return fakePreprocessor1 }], - 'preprocessor:fake2': ['factory', function () { return fakePreprocessor2 }] - }, emitterSetting]) + const fakes = { + fake1: sinon.spy((content, file, done) => { + file.path = file.path + '-p1' + done(null, content + '-c1') + }), + fake2: sinon.spy((content, file, done) => { + file.path = file.path + '-p2' + done(content + '-c2') + }) + } + function fakeInstatiatePlugin (kind, name) { + return fakes[name] + } - const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake1', 'fake2'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake1', 'fake2'] }, {}, null, fakeInstatiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } await pp(file) - expect(fakePreprocessor1).to.have.been.calledOnce - expect(fakePreprocessor2).to.have.been.calledOnce + expect(fakes.fake1).to.have.been.calledOnce + expect(fakes.fake2).to.have.been.calledOnce expect(file.path).to.equal('path-p1-p2') expect(file.content).to.equal('content-c1-c2') }) it('should compute SHA', async () => { - const pp = m.createPriorityPreprocessor({}, {}, null, new di.Injector([emitterSetting])) + const pp = m.createPriorityPreprocessor({}, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } await pp(file) @@ -198,15 +177,11 @@ describe('preprocessor', () => { }) it('should compute SHA from content returned by a processor', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, content + '-processed') }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/a.js': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/a.js': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const fileProcess = { originalPath: '/some/a.js', path: 'path' } const fileSkip = { originalPath: '/some/b.js', path: 'path' } @@ -221,15 +196,11 @@ describe('preprocessor', () => { }) it('should return error if any preprocessor fails', () => { - const failingPreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(new Error('Some error'), null) }) - const injector = new di.Injector([{ - 'preprocessor:failing': ['factory', function () { return failingPreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*.js': ['failing'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['failing'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } @@ -241,20 +212,20 @@ describe('preprocessor', () => { }) it('should stop preprocessing after an error', async () => { - const failingPreprocessor = sinon.spy((content, file, done) => { - done(new Error('Some error'), null) - }) - - const fakePreprocessor = sinon.spy((content, file, done) => { - done(null, content) - }) + const fakes = { + failing: sinon.spy((content, file, done) => { + done(new Error('Some error'), null) + }), + fake: sinon.spy((content, file, done) => { + done(null, content) + }) + } - const injector = new di.Injector([{ - 'preprocessor:failing': ['factory', function () { return failingPreprocessor }], - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) + function fakeInstantiatePlugin (kind, name) { + return fakes[name] + } - const pp = m.createPriorityPreprocessor({ '**/*.js': ['failing', 'fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['failing', 'fake'] }, {}, null, fakeInstantiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } @@ -263,7 +234,7 @@ describe('preprocessor', () => { }, (err) => { expect(err.message).to.equal('Some error') }) - expect(fakePreprocessor).not.to.have.been.called + expect(fakes.fake).not.to.have.been.called }) describe('when fs.readFile fails', () => { @@ -274,15 +245,11 @@ describe('preprocessor', () => { }) it('should retry up to 3 times', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, content) }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) await pp(file).then(() => { throw new Error('Should be rejected') @@ -295,9 +262,7 @@ describe('preprocessor', () => { }) it('should throw after 3 retries', async () => { - const injector = new di.Injector([{}, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*.js': [] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*.js': [] }, {}, null, simpleFakeInstantiatePlugin) await pp(file).then(() => { throw new Error('Should be rejected') @@ -309,15 +274,11 @@ describe('preprocessor', () => { }) it('should not preprocess binary files by default', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, content) }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/photo.png', path: 'path' } @@ -327,15 +288,11 @@ describe('preprocessor', () => { }) it('should not preprocess files configured to be binary', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, content) }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/proto.pb', path: 'path', isBinary: true } @@ -345,15 +302,11 @@ describe('preprocessor', () => { }) it('should preprocess files configured not to be binary', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, content) }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) // Explicit false for isBinary const file = { originalPath: '/some/proto.pb', path: 'path', isBinary: false } @@ -364,16 +317,12 @@ describe('preprocessor', () => { }) it('should preprocess binary files if handleBinaryFiles=true', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, content) }) fakePreprocessor.handleBinaryFiles = true - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { return fakePreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/photo.png', path: 'path' } @@ -383,15 +332,11 @@ describe('preprocessor', () => { }) it('should not preprocess binary files with capital letters in extension', async () => { - const fakePreprocessor = sinon.spy((content, file, done) => { + fakePreprocessor = sinon.spy((content, file, done) => { done(null, content) }) - const injector = new di.Injector([{ - 'preprocessor:fake': ['factory', function () { fakePreprocessor }] - }, emitterSetting]) - - const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, injector) + const pp = m.createPriorityPreprocessor({ '**/*': ['fake'] }, {}, null, simpleFakeInstantiatePlugin) const file = { originalPath: '/some/CAM_PHOTO.JPG', path: 'path' } @@ -402,72 +347,68 @@ describe('preprocessor', () => { it('should merge lists of preprocessors using default priority', async () => { const callOrder = [] - const fakePreprocessorA = sinon.spy((content, file, done) => { - callOrder.push('a') - done(null, content) - }) - const fakePreprocessorB = sinon.spy((content, file, done) => { - callOrder.push('b') - done(null, content) - }) - const fakePreprocessorC = sinon.spy((content, file, done) => { - callOrder.push('c') - done(null, content) - }) - const fakePreprocessorD = sinon.spy((content, file, done) => { - callOrder.push('d') - done(null, content) - }) - - const injector = new di.Injector([{ - 'preprocessor:fakeA': ['factory', function () { return fakePreprocessorA }], - 'preprocessor:fakeB': ['factory', function () { return fakePreprocessorB }], - 'preprocessor:fakeC': ['factory', function () { return fakePreprocessorC }], - 'preprocessor:fakeD': ['factory', function () { return fakePreprocessorD }] - }, emitterSetting]) + const fakes = { + fakeA: sinon.spy((content, file, done) => { + callOrder.push('a') + done(null, content) + }), + fakeB: sinon.spy((content, file, done) => { + callOrder.push('b') + done(null, content) + }), + fakeC: sinon.spy((content, file, done) => { + callOrder.push('c') + done(null, content) + }), + fakeD: sinon.spy((content, file, done) => { + callOrder.push('d') + done(null, content) + }) + } + function fakeInstantiatePlugin (kind, name) { + return fakes[name] + } const pp = m.createPriorityPreprocessor({ '/*/a.js': ['fakeA', 'fakeB'], '/some/*': ['fakeB', 'fakeC'], '/some/a.js': ['fakeD'] - }, {}, null, injector) + }, {}, null, fakeInstantiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } await pp(file) - expect(fakePreprocessorA).to.have.been.called - expect(fakePreprocessorB).to.have.been.called - expect(fakePreprocessorC).to.have.been.called - expect(fakePreprocessorD).to.have.been.called + expect(fakes.fakeA).to.have.been.called + expect(fakes.fakeB).to.have.been.called + expect(fakes.fakeC).to.have.been.called + expect(fakes.fakeD).to.have.been.called expect(callOrder).to.eql(['a', 'b', 'c', 'd']) }) it('should merge lists of preprocessors obeying priority', async () => { const callOrder = [] - const fakePreprocessorA = sinon.spy((content, file, done) => { - callOrder.push('a') - done(null, content) - }) - const fakePreprocessorB = sinon.spy((content, file, done) => { - callOrder.push('b') - done(null, content) - }) - const fakePreprocessorC = sinon.spy((content, file, done) => { - callOrder.push('c') - done(null, content) - }) - const fakePreprocessorD = sinon.spy((content, file, done) => { - callOrder.push('d') - done(null, content) - }) - - const injector = new di.Injector([{ - 'preprocessor:fakeA': ['factory', function () { return fakePreprocessorA }], - 'preprocessor:fakeB': ['factory', function () { return fakePreprocessorB }], - 'preprocessor:fakeC': ['factory', function () { return fakePreprocessorC }], - 'preprocessor:fakeD': ['factory', function () { return fakePreprocessorD }] - }, emitterSetting]) + const fakes = { + fakeA: sinon.spy((content, file, done) => { + callOrder.push('a') + done(null, content) + }), + fakeB: sinon.spy((content, file, done) => { + callOrder.push('b') + done(null, content) + }), + fakeC: sinon.spy((content, file, done) => { + callOrder.push('c') + done(null, content) + }), + fakeD: sinon.spy((content, file, done) => { + callOrder.push('d') + done(null, content) + }) + } + function fakeInstantiatePlugin (kind, name) { + return fakes[name] + } const priority = { fakeA: -1, fakeB: 1, fakeD: 100 } @@ -475,15 +416,15 @@ describe('preprocessor', () => { '/*/a.js': ['fakeA', 'fakeB'], '/some/*': ['fakeB', 'fakeC'], '/some/a.js': ['fakeD'] - }, priority, null, injector) + }, priority, null, fakeInstantiatePlugin) const file = { originalPath: '/some/a.js', path: 'path' } await pp(file) - expect(fakePreprocessorA).to.have.been.called - expect(fakePreprocessorB).to.have.been.called - expect(fakePreprocessorC).to.have.been.called - expect(fakePreprocessorD).to.have.been.called + expect(fakes.fakeA).to.have.been.called + expect(fakes.fakeB).to.have.been.called + expect(fakes.fakeC).to.have.been.called + expect(fakes.fakeD).to.have.been.called expect(callOrder).to.eql(['d', 'b', 'c', 'a']) }) diff --git a/test/unit/web-server.spec.js b/test/unit/web-server.spec.js index 8517f172e..fccf57b65 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],