Skip to content

Latest commit

 

History

History
310 lines (251 loc) · 8.35 KB

extending.md

File metadata and controls

310 lines (251 loc) · 8.35 KB

Extending

Intern‘s functionality can be extended using user scripts and third party libraries.

The plugin mechanism

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 to registerPlugin 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');
});

Pre- and post-test code

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

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.

Interfaces

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', () => {
        ...
    });
});

Loaders

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.