Testing a plugin
@triiiceratops/plugin-sdk/testing lets you validate a plugin without a full
application. It mounts your plugin against a test viewer context: a real,
compiled ViewerState (real commands, real batched notifications) with recording
doubles for the style, UI, and locale services and an injectable OSD stub that
defaults to absent. The harness is fake; the state is never fake, so a passing
test reflects production semantics.
The kit runs in a plain vitest project — no Svelte tooling required — because the
headless state comes from core's compiled triiiceratops/testing entry.
The flush timing rule
Notifications are batched and delivered on the reactive flush, never
synchronously inside a command. After a command (or a locale/OSD change),
await flush() before asserting a subscriber reacted:
import { describe, it, expect } from 'vitest';
import { createTestViewerContext, flush } from '@triiiceratops/plugin-sdk/testing';
describe('viewer state notifications', () => {
it('delivers on the flush, not synchronously', async () => {
const { context } = createTestViewerContext();
const open = context.selectors.select((s) => s.toolbarOpen);
let seen = open.get();
open.subscribe((v) => {
seen = v;
});
context.viewerState.toggleToolbar();
// Batched: no synchronous delivery yet.
expect(seen).toBe(false);
await flush();
expect(seen).toBe(true);
});
});
Mounting a plugin against real state
Activate your plugin against the context's live state and assert its DOM and cleanup. The recording style service records installs so you can assert they were released on deactivation:
import { describe, it, expect } from 'vitest';
import { activatePlugin } from '@triiiceratops/plugin-sdk';
import { createTestViewerContext, flush } from '@triiiceratops/plugin-sdk/testing';
import {
CORE_VERSION,
pluginApiVersion,
capabilities,
} from 'triiiceratops/testing';
import { createExamplePlugin } from './my-plugin';
describe('example plugin', () => {
it('reacts to a command and cleans up', async () => {
const tc = createTestViewerContext();
const container = document.createElement('div');
const activation = activatePlugin(createExamplePlugin(), {
container,
viewerState: tc.viewerState,
coreVersion: CORE_VERSION,
pluginApiVersion,
capabilities,
styles: tc.styles,
locale: tc.locale,
ui: tc.ui,
});
const label = container.querySelector('span');
expect(label?.textContent).toBe('closed');
tc.viewerState.toggleToolbar();
await flush();
expect(label?.textContent).toBe('open');
activation.deactivate();
expect(tc.styles.installed.every((s) => s.released)).toBe(true);
});
});
The conformance suite
runPluginConformance runs the whole SDK lifecycle battery against your plugin
factory. It registers its own describe/it blocks, so call it at the top level
of a test file:
import { runPluginConformance } from '@triiiceratops/plugin-sdk/testing';
import { createExamplePlugin } from './my-plugin';
runPluginConformance(() => createExamplePlugin());
OSD-dependent behavior
The kit ships no OSD or Annotorious fake. Inject a caller-supplied stub with
setOsdViewer(...) to exercise the readiness path, but validate genuine
OSD-dependent behavior at the browser seam instead:
import { createTestViewerContext, whenOsdReady } from '@triiiceratops/plugin-sdk/testing';
async function readinessExample() {
const tc = createTestViewerContext();
const ready = whenOsdReady(tc.viewerState);
tc.setOsdViewer({ viewport: {} }); // your stub
await ready;
}
Testing an annotation storage adapter
Annotation-editor adapters have their own conformance API in
@triiiceratops/plugin-annotation-editor/testing. It checks
load/create/update/delete round-trips, verbatim body preservation, manifest and
canvas isolation, and — when you opt in — server-assigned ids and hydrate. It too
registers its own describe/it blocks:
import { runAdapterContractTests } from '@triiiceratops/plugin-annotation-editor/testing';
import { LocalStorageAdapter } from '@triiiceratops/plugin-annotation-editor';
runAdapterContractTests(() => new LocalStorageAdapter(), {
supportsIdReconciliation: false,
supportsHydrate: false,
});
vitest is the only extra requirement, pulled in through the testing subpath —
it never becomes a runtime dependency of your plugin.
Verified against the packed packages
CI runs the vitest-kit fixture (the SDK test kit against the compiled
triiiceratops/testing entry in a plain vitest project) and the
plugin-annotation-conformance fixture (the adapter conformance suite from the
packed @triiiceratops/plugin-annotation-editor/testing subpath).