Skip to content

Testing plugins

Plugins are easy to test because their logic is pure and their sync is a CRDT. Three layers, all under bun test:

Test derivations directly, with no doc and no browser:

src/logic.test.ts
import { test, expect } from "bun:test";
import { rankedQuestions, voteCount } from "./logic";
test("ranks by votes desc, then oldest first", () => {
const state = {
questions: { a: { text: "A", author: "x", ts: 1 }, b: { text: "B", author: "y", ts: 2 } },
votes: { "a|p1": true, "b|p1": true, "b|p2": true },
answered: {}, dismissed: {},
};
expect(rankedQuestions(state).map((q) => q.id)).toEqual(["b", "a"]);
expect(voteCount(state, "b")).toBe(2);
});

Prove that concurrent edits merge, without a browser, by syncing two Y.Docs and asserting the derived view agrees:

src/logic.test.ts
import * as Y from "yjs";
import { pluginState } from "@liebstoeckel/plugin-sdk";
import { qaSchema, rankedQuestions } from "./logic";
function sync(a: Y.Doc, b: Y.Doc) {
a.on("update", (u) => Y.applyUpdate(b, u));
b.on("update", (u) => Y.applyUpdate(a, u));
}
test("a question on one client, an upvote on another, converge", () => {
const docA = new Y.Doc(), docB = new Y.Doc();
sync(docA, docB);
const A = pluginState(docA, "qa", qaSchema);
const B = pluginState(docB, "qa", qaSchema);
A.recordSet("questions", "q1", { text: "Why Bun?", author: "p1", ts: 1 });
B.recordSet("votes", "q1|p2", true);
expect(rankedQuestions(A.snapshot())).toEqual(rankedQuestions(B.snapshot()));
});

Confirm the surfaces render without throwing, using renderToStaticMarkup:

src/client.test.tsx
import { renderToStaticMarkup } from "react-dom/server";
import * as Y from "yjs";
import { pluginState, type ClientProps } from "@liebstoeckel/plugin-sdk";
import plugin from "./client";
import { mySchema, type MyState } from "./logic";
const props = (): ClientProps<MyState> => {
const doc = new Y.Doc();
return {
doc, state: pluginState(doc, "myplugin", mySchema), snapshot: mySchema.default(),
role: "viewer", live: true, participantId: "p1",
theme: { viz: ["#fff"] } as never, ui: {}, props: {},
};
};
test("Slide renders", () => {
expect(renderToStaticMarkup(<plugin.client.Slide {...props()} />)).toContain("");
});

For a plugin with a server part, drive it against a Hub (the live relay class) and assert its effect reaches a connected peer, the same pattern the live-server integration tests use.

Comments

liebstoeckel

Code-first presentations your agent can author. One file out, no server.

© 2026 Leon Kaucher