Testing plugins
Mount a plugin folder, exercise its Actions and inspect state with automatic cleanup.
mountPlugin from @bitakit/core/testing runs one plugin folder without React or a framework.
It uses the same discovery conventions as an app and awaits service startup before returning.
Generate a first test
In an app, the CLI writes a starting test for an existing plugin:
npx bitakit add test countertests/counter.test.ts mounts src/plugins/counter, checks that the plugin starts, then stops
it. When the plugin places Actions in shell.* slots, the file includes the
Shell stand-in. The command then prints what the app still
needs as commands: the dev dependencies below that package.json does not list,
npm pkg set type=module and npm pkg set scripts.test="node --test tests/*.test.ts". It never
edits package.json or overwrites a file. Add the behavior you want to keep, as in the next
section.
Test the Counter
Use the service and increment Action from Add state and Add an Action, before adding Settings. From that project's root, install the test dependencies:
npm install --save-dev @bitakit/discovery jiti typescript @types/nodeUse Node.js 24 or newer and "type": "module" in package.json. Create
tests/counter.test.ts. The type-only import derives the service type from its existing
definition; it does not start or register another service.
import assert from 'node:assert/strict';
import test from 'node:test';
import { defineContract, getService } from '@bitakit/core';
import { mountPlugin } from '@bitakit/core/testing';
import type counterValue from '../src/plugins/counter/services/value.ts';
const counterService = defineContract('counter.value').of<ReturnType<typeof counterValue.create>>();
test('increment changes the shared Counter value', async () => {
const plugin = await mountPlugin('src/plugins/counter');
try {
const counter = getService(plugin.services, counterService);
assert.equal(counter.state.get().count, 0);
const result = await plugin.execute('increment');
assert.equal(result.status, 'completed');
assert.equal(counter.state.get().count, 1);
} finally {
await plugin.stop();
}
});node --test tests/counter.test.tsNode strips types; it does not typecheck. Include the test in a strict TypeScript check
with noEmit and allowImportingTsExtensions. Keep headless plugin files free of JSX;
the helper skips ui/ and does not verify rendering.
A plugin in the Shell navigation
A plugin made by npx bitakit add plugin places its open Action in shell.navigation.
Without Shell, startup fails with Slot "shell.navigation": no owning definition exists.
Pass a stand-in for the slot owner:
import { definePlugin, defineSlot } from '@bitakit/core';
const shell = definePlugin({
id: 'shell',
slots: { navigation: defineSlot({ accepts: 'actions' }) },
});
const plugin = await mountPlugin('src/plugins/notes', { plugins: [shell] });Two plugins together
plugins also takes plugin folders. Mount the listener and pass the emitter's folder, so
the test runs both real plugins and neither needs a hand-written stand-in:
import type feed from '../src/plugins/activity/services/feed.ts';
const feedService = defineContract('activity.feed').of<ReturnType<typeof feed.create>>();
const plugin = await mountPlugin('src/plugins/activity', { plugins: ['src/plugins/notes'] });
await plugin.execute('notes.add', 'Ship');
assert.deepEqual(getService(plugin.services, feedService).state.get().titles, ['Ship']);Options and returned handles
| Option | Purpose |
|---|---|
id | Override the ID inferred from the manifest, plugin folder or package. |
plugins | Other plugins for this test: plugin folders or definePlugin objects (stubs). |
services | Supply host-owned services, for example a fake auth implementation. |
configOverrides | Override developer configuration for the test. |
The result contains id, runtime, app, actions, services, events, execute and
stop. execute('increment') uses the mounted plugin's prefix; full Action IDs also work.
Generated or manually augmented ActionMap entries type full IDs and their inputs/results.
events.recorded contains { type, data } for events emitted while mounted, including
startup. Use it to assert cross-plugin notifications after a mutation. A failed startup
cleans up the partial runtime and rejects with the attributed startup error. Always await
stop() before replacing a runtime or finishing a test; cleanup failures are test failures.
For UI tests, renderWithBitaKit from @bitakit/ui/testing mounts the existing provider
and awaits readiness. The test must supply a DOM environment such as jsdom; the helper
does not install one. See the package's testing entry for its typed options.