Intern‘s functionality can be extended using user scripts and third party libraries.
The “plugin” mechanism is a cross-environment method for adding functionality to
Intern. Plugins are registered using the
plugins config property and loaded by
Intern using an environment’s native code loading mechanism: require
in Node
and script injection in the browser. If an external loader has been configured
using the loader
property, plugins can be marked to use the loader with a
useLoader
property.
A plugin can register resources with Intern that may be used in tests and suites, or it can also alter Intern’s functionality in some way, or even modify the environment itself.
For example, a plugin that provided tests with access to a MongoDB database might look like:
intern.registerPlugin('dbaccess', async options => {
const connect = promisify(MongoClient.connect);
const db = await connect(options.dbUrl);
return { db };
});
Within a suite, the plugin would be accessed like:
const { db } = intern.getPlugin('dbaccess');
A third party script such as ts-node/register
may also be loaded as a plugin.
For example, loading ts-node/register
as a plugin will allow Intern to load
TypeScript modules directly (in Node only):
{
plugins: 'node_modules/ts-node/register/index.js'
}
💡The plugin registration mechanism (
registerPlugin
) isn’t necessary in environments with modules loaders since tests may load extension code using standard loader mechanisms (e.g.,require
). It is most useful for environments where a module loader may not be present, such as when testing legacy code in a browser.
⚠️ When loading a plugin without a module loader, the call toregisterPlugin
must be synchronous.
Note that when loading a plugin without a module loader, the call to
registerPlugin
must be made synchronously. In other words, a plugin generally
shouldn’t do this:
// tests/plugin.js
System.import('some_module').then(function(module) {
intern.registerPlugin('foo', function() {
return module;
});
});
Instead, do this:
// tests/plugin.js
intern.registerPlugin('foo', function() {
return System.import('some_module');
});
Code that needs to run before or after the testing process can run in beforeRun or afterRun event listeners:
// tests/setup.ts
intern.on('beforeRun', () => {
// code
});
To load this module using ts-node:
{
plugins: ['node_modules/ts-node/register/index.js', 'tests/setup.ts']
}
As with all Intern event listeners the callback may run asynchronous code. Async callbacks should return a Promise that resolves when the async code has completed.
Reporters are code that registers for Intern events. For example, a reporter that displays test results to the console could be as simple as:
// tests/myReporter.ts
intern.on('testEnd', test => {
if (test.skipped) {
console.log(`${test.id} skipped`);
} else if (test.error) {
console.log(`${test.id} failed`);
} else {
console.log(`${test.id} passed`);
}
});
If the reporter needs a bit more config, or needs to take some async action
during initialization, it can use the registerPlugin
mechanism:
intern.registerPlugin('myReporter', options => {
return fetch(options.template).then(templateSource => {
const template = JSON.parse(templateSource);
intern.on('testEnd', test => {
if (test.skipped) {
console.log(template.skipped.replace(/{test}/, test.id));
} else if (test.error) {
console.log(template.error.replace(/{test}/, test.id));
} else {
console.log(template.passed.replace(/{test}/, test.id));
}
});
});
});
Load the reporter as a plugin:
{
plugins: '_build/tests/myReporter.js'
}
If a reporter takes options, they can be passed through an options
property on
a plugin descriptor:
{
plugins: {
script: '_build/tests/myReporter.js',
options: {
filename: 'report.txt'
}
}
}
Intern provides several built-in reporters that can be enabled via the
reporters config option. User/custom reporters can simply register for Intern
events; they do not need to use the reporters
config property.
An interface is an API for registering test suites. Intern has several built in
interfaces, such as object and
bdd. These interfaces all work by creating Suite and
Test objects and registering them with Intern’s root suite(s). New interfaces
should follow the same pattern. For example, below is an excerpt from the tdd
interface, which allows suites to be registered using suite
and test
functions:
import Suite from '../Suite';
import Test from '../Test';
import intern from '../../intern';
let currentSuite;
export function suite(name, factory) {
if (!currentSuite) {
executor.addSuite(parent => {
currentSuite = parent;
registerSuite(name, factory);
currentSuite = null;
});
} else {
registerSuite(name, factory);
}
}
export function test(name, test) {
if (!currentSuite) {
throw new Error('A test must be declared within a suite');
}
currentSuite.add(new Test({ name, test }));
}
function registerSuite(name, factory) {
const parent = currentSuite!;
currentSuite = new Suite({ name, parent });
parent.add(currentSuite);
factory(currentSuite);
currentSuite = parent;
}
An interface plugin would define and register its interface methods:
// myInterface.ts
intern.registerPlugin('myInterface', async options => {
function suite(...) {
}
function test(...) {
}
return { suite, test };
});
// someSuite.ts
const { suite, test } = intern.getPlugin('myInterface');
suite('foo', () => {
test('test1', () => {
...
});
});
Loader scripts will generally be very simple; the main requirement is that the script is standalone (i.e., not a module itself). For example, the built-in ‘dojo’ loader script looks like the following:
intern.registerLoader(options => {
const globalObj: any = typeof window !== 'undefined' ? window : global;
options.baseUrl = options.baseUrl || intern.config.basePath;
if (!('async' in options)) {
options.async = true;
}
// Setup the loader config
globalObj.dojoConfig = loaderConfig;
// Load the loader using intern.loadScript, which loads simple scripts via injection
return intern.loadScript('node_modules/dojo/dojo.js').then(() => {
const require = globalObj.require;
// Return a function that can be used to load modules with the loader
return (modules: string[]) => {
let handle: { remove(): void };
return new Promise<void>((resolve, reject) => {
handle = require.on('error', (error: Error) => {
intern.emit('error', error);
reject(new Error(`Dojo loader error: ${error.message}`));
});
// The module loader function doesn't return modules, it just loads them
require(modules, () => {
resolve();
});
}).then(
() => {
handle.remove();
},
error => {
handle && handle.remove();
throw error;
}
);
};
});
});
See configuring loaders for more information about how to load and pass options to a custom loader.