Skip to content

Commit

Permalink
docs(plugins): add more information about plugins (#3649)
Browse files Browse the repository at this point in the history
Changes:

- Promote `require('karma-plugin')` form over `'karma-plugin'` form. Former makes it more clear that plugin is imported from an NPM package and it is a regular JS object, there is no magic behind it. This is inspired by #3498 where user is not aware that it is even possible. This also should make it easier with plug'n'play package managers (like Yarn 2).
- Explain that `plugins` array does not activate plugins, but only registers them to clarify #1247 (comment).
- Explain the plugin structure, DI and how to build a new plugin.
- Re-arrange "Developing plugins" page to make it easier to add more information about every plugin type. Adding actual information should be done in the separate PRs though.

Fixes #1247
  • Loading branch information
devoto13 authored Feb 12, 2021
1 parent 474f4e1 commit 99908c3
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 73 deletions.
10 changes: 3 additions & 7 deletions docs/config/01-configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
50 changes: 30 additions & 20 deletions docs/config/05-plugins.md
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -22,26 +23,35 @@ Therefore, a simple way to install a plugin is:
npm install karma-<plugin name> --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
184 changes: 140 additions & 44 deletions docs/dev/05-plugins.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function resolve (plugins, emitter) {
.filter((pluginName) => !IGNORED_PACKAGES.includes(pluginName) && regexp.test(pluginName))
.forEach((pluginName) => requirePlugin(`${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}`)
Expand Down

0 comments on commit 99908c3

Please sign in to comment.