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.
Karma is assembled using dependency injection. It is important to understand this concept to be able to develop plugins.
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.
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.
Each plugin is essentially a service with its associated DI token. When user activates a plugin 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.
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.
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.
// 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]
};
// 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']
})
}
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.
// 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.
This section outlines API requirements and conventions for different plugin types. There also links to some plugins, which you can use for inspiration.
karma-*
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
to send results back to karma:
.result
a single test has finished.complete
the client completed execution of all the tests.error
an error happened in the client.info
other data (e.g. number of tests or debugging messages)Most commonly you'll use the result
method to send individual test success or failure
statuses. The method takes an object of the form:
{
// test id
id: String,
// test description
description: String,
// the suite to which this test belongs. potentially nested.
suite: Array[String],
// an array of string error messages that might explain a failure.
// this is required if success is false.
log: Array[String],
success: Boolean, // pass / fail
skipped: Boolean // skipped / ran
}
karma-*-reporter
karma-plugin
, karma-reporter
karma-*-launcher
karma-plugin
, karma-launcher
karma-*-preprocessor
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.
content
of the file being processedfile
object describing the file being processedsome/file.coffee
-> some/file.coffee.js
This path is mutable and may not actually exist.next(null, encodedContent)
next
function to be called when preprocessing is complete, should be called as next(null, processedContent)
or next(error)
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.