Testing plugins
Plugins are easy to test because their logic is pure and their sync is a CRDT. Three layers, all under bun test:
1. Pure logic
Section titled “1. Pure logic”Test derivations directly, with no doc and no browser:
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);});2. Convergence (two synced docs)
Section titled “2. Convergence (two synced docs)”Prove that concurrent edits merge, without a browser, by syncing two Y.Docs and asserting the derived view agrees:
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()));});3. Render smoke test
Section titled “3. Render smoke test”Confirm the surfaces render without throwing, using renderToStaticMarkup:
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("…");});Server effects
Section titled “Server effects”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.
Building a plugin The full plugin walkthrough these tests belong to.
liebstoeckel
Code-first presentations your agent can author. One file out, no server.
© 2026 Leon Kaucher
Comments