Skip to main content

Developing Plugins

Plugins allow you to add custom UI components to the HTML Reporter interface. For example, you can display test stability statistics or add a button for integration with an external service. HTML Reporter has several extension points where the plugin UI can be displayed — for example, on the settings panel or on the test result page.

Quick Start

A fully working plugin with configured build and using the main features of the plugin system is available in the HTML Reporter repository.

We recommend using this example as a template for your plugin.

How Plugins Work

Plugins are connected to HTML Reporter in the config, for example:

testplane.config.ts
export default {
plugins: {
"html-reporter/testplane": {
pluginsEnabled: true,
plugins: [
{
name: "my-plugin-package",
component: "MyPlugin",
point: "result_meta",
position: "after",
},
],
},
},
};

In this case, HTML Reporter will look for the plugin in the npm package my-plugin-package, specifically:

  • In my-plugin-package/plugin.js HTML Reporter will look for the implementation of the MyPlugin component, which will be displayed at the result_meta extension point
  • In my-plugin-package/middleware.js HTML Reporter will look for the implementation of server endpoints

Therefore, a typical plugin consists of three parts:

my-plugin/
├── ui/ # Plugin UI — built into plugin.js bundle using Vite
├── server/ # Server part of the plugin (optional) — entry point expected in middleware.js
└── preset-for-config/ # Preset for convenient connection (optional)

Creating Plugin UI

1. Build Setup

Output File Format

The result of building the plugin UI should be 1 file — a plugin.js bundle in the following format:

ui/build/plugin.js
__testplane_html_reporter_register_plugin__([
"react",
function (React, options) {
// ...
return { MyPlugin, reducers: [] };
},
]);

The file should contain only a call to the __testplane_html_reporter_register_plugin__ function.

The only argument of the function is an array. It declares the dependencies needed by the plugin (see available dependencies in the Plugins SDK reference). The last element of the array is a function that will be called with the listed dependencies and should return an object with the component and reducers.

For more details on what exactly this function is called with, see the HTML Reporter source code.

Reusing Dependencies

For React and hooks to work correctly, it is critical that dependencies are not duplicated, and the same instances are used everywhere — both in HTML Reporter and in the plugin.

HTML Reporter provides a number of dependencies available for reuse, including React and Gravity UI.

To reuse these dependencies, you need to specify them in external in the Vite configuration.

Vite Configuration Example

In practice, the required bundle characteristics are achieved by building through Vite with external dependencies and a special wrapper. You can see an example in the HTML Reporter repository.

2. Component Implementation

Depending on which extension point is used, HTML Reporter may pass some data in props, for example, the current test run result (see more in the Plugins SDK reference):

ui/Plugin.tsx
import { Button } from "@gravity-ui/uikit";

export const MyPlugin = ({ result }: { result: { id: string; suitePath: string[] } }) => {
const fullName = result.suitePath.join(" ");

return (
<div>
<div>Test Full Name: {fullName}</div>
<Button onClick={() => window.open(`example-tms.com/test/${result.id}`, "_blank")}>
Open test in TMS
</Button>
</div>
);
};

The main plugin file can be organized like this:

ui/index.tsx
import { MyPlugin } from "./Plugin";

export default {
MyPlugin,
reducers: [],
};

When developing plugins, it is recommended to use Gravity UI and components from the Plugins SDK for visual consistency with the rest of the HTML Reporter interface.

Working with Redux (Optional)

Use Redux to store and retrieve plugin data.

Creating Actions

Define action types with a unique prefix:

ui/actions.ts
export const actions = {
LOADING: "plugins/myPlugin/loading",
LOADED: "plugins/myPlugin/loaded",
ERROR: "plugins/myPlugin/error",
} as const;
Creating Thunk for Requests

Use thunk actions for asynchronous operations:

ui/actions.ts
export const fetchData = (resultId: string) => async dispatch => {
dispatch({ type: actions.LOADING, payload: { resultId } });

const endpoint = `${pluginOptions.pluginServerEndpointPrefix}/data`;
const { data } = await axios.get(endpoint);

dispatch({ type: actions.LOADED, payload: { resultId, data } });
};

The global variable pluginOptions.pluginServerEndpointPrefix contains the base URL for requests to the plugin server.

Creating Reducer

The reducer updates the plugin state. Use Immer for immutable updates:

ui/reducers.ts
import produce from "immer";

export default produce((draft, action) => {
if (!draft.plugins.myPlugin) {
draft.plugins.myPlugin = { byResultId: {} };
}

switch (action.type) {
case actions.LOADING: {
const { resultId } = action.payload;
draft.plugins.myPlugin.byResultId[resultId] = {
...defaultResultState,
status: "loading",
};
break;
}
// ...
}
});
Reading State

Use useSelector to read data from the store:

const data = useSelector(state => state.plugins.myPlugin?.byResultId[result.id]);

For more details on the Redux store structure, see the HTML Reporter source code.

The component name (MyPlugin) must match the component field in the plugin configuration.

3. Server Part (Optional)

The server part receives an Express Router with the prefix /plugin-routes/{plugin-name}/:

server/index.ts
import type { Router } from "express";

export = function (router: Router) {
router.get("/data", (req, res) => {
res.json({ value: 42 });
});
};

This endpoint will be available at /plugin-routes/my-plugin/data.

It is recommended to get the server part address from the global variable pluginOptions.pluginServerEndpointPrefix, as the plugin name is transformed on the HTML Reporter side and may not match the package name.

4. Configuration Preset (Optional)

The preset simplifies plugin connection for users:

preset-for-config/index.ts
import { ExtensionPointName } from "html-reporter/plugins-sdk";

export = function (config = {}) {
return [
{
name: "my-plugin-package",
component: "MyPlugin",
point: ExtensionPointName.ResultMeta,
position: "after",
config,
},
];
};

Thanks to the preset, instead of specifying the full plugin descriptor in the config, users can import this object and use it.

5. Connecting the Plugin

After publishing, users connect the plugin like this:

testplane.config.ts
import myPlugin from "my-plugin-package";

export default {
plugins: {
"html-reporter/testplane": {
pluginsEnabled: true,
plugins: myPlugin(),
},
},
};

Additional Materials