Testing Extensions
Lumio extensions are React applications with server functions. This guide covers unit testing components, testing server functions, and manual testing against a real overlay before submitting for review.
Testing strategy
| Level | What to test | Tool |
|---|---|---|
| Unit | Pure functions, validators, data transforms | Vitest or Jest |
| Component | Rendering logic, UI state, config reads | React Testing Library |
| Integration | SDK surfaces, events, actions, and server or bot handlers | @zaflun/lumio-devkit |
| Manual (local) | Full flow: editor → layer → events | lumio dev |
| Manual (live) | Live overlay in browser source with real events | lumio deploy of a release candidate |
Setting up Vitest
pnpm add -D vitest @testing-library/react @testing-library/user-event jsdom
vitest.config.ts:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
globals: true,
},
});
package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
Testing server function handlers
For a test that exercises the SDK host boundary and the local server runtime together, use the devkit below. A small hand-built ctx remains useful for testing a pure handler in isolation.
Server functions are plain async functions - test the handler directly by constructing a minimal ctx mock.
// server/functions.test.ts
import { describe, it, expect, vi } from "vitest";
import { incrementScore } from "./functions";
function makeCtx(storage: Record<string, unknown> = {}) {
return {
db: {
get: vi.fn(async (_scope, ...keys) => storage[keys.join("/")] ?? null),
set: vi.fn(async (_scope, ...keysAndValue) => {
const value = keysAndValue[keysAndValue.length - 1];
const keys = (keysAndValue.slice(0, -1) as string[]).join("/");
storage[keys] = value;
}),
delete: vi.fn(),
list: vi.fn(async () => []),
},
fetch: vi.fn(),
secrets: { get: vi.fn((k: string) => `mock_${k}`), has: vi.fn(() => true) },
realtime: { push: vi.fn() },
identity: null,
};
}
describe("incrementScore", () => {
it("starts at 0 and increments", async () => {
const ctx = makeCtx();
const result = await incrementScore.handler(ctx, { amount: 5 });
expect(result.score).toBe(5);
});
it("adds to existing score", async () => {
const ctx = makeCtx({ score: 10 });
const result = await incrementScore.handler(ctx, { amount: 3 });
expect(result.score).toBe(13);
});
it("pushes a realtime event", async () => {
const ctx = makeCtx();
await incrementScore.handler(ctx, { amount: 1 });
expect(ctx.realtime.push).toHaveBeenCalledWith("score:updated", { score: 1 });
});
});
Testing React components
Use React Testing Library to render surfaces and assert on output.
// src/layer.test.tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ScoreDisplay } from "./layer";
// Mock the SDK hooks
vi.mock("@zaflun/lumio-sdk", () => ({
useLumioConfig: () => ({
config: { homeTeam: "LAL", awayTeam: "BOS" },
setConfig: vi.fn(),
loading: false,
}),
useLumioTheme: () => ({ mode: "dark", primaryColor: "#6366f1", fontFamily: "sans-serif", borderRadius: 8 }),
useLumioEvent: () => null,
Box: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => <div {...props}>{children}</div>,
Text: ({ content }: { content: string }) => <span>{content}</span>,
}));
describe("ScoreDisplay", () => {
it("renders team names from config", () => {
render(<ScoreDisplay />);
expect(screen.getByText("LAL")).toBeInTheDocument();
expect(screen.getByText("BOS")).toBeInTheDocument();
});
});
Testing with the mock host
Install @zaflun/lumio-devkit as a development dependency (projects scaffolded by lumio init list it already). It runs the real SDK hooks against a mock Worker RPC host; no account, stream, or browser is required.
import { afterEach, expect, it } from "vitest";
import { createMockHost, type MockHost } from "@zaflun/lumio-devkit";
import { FollowerAlert } from "../src/FollowerAlert";
let host: MockHost;
afterEach(() => host?.destroy());
it("renders a follower alert", async () => {
host = createMockHost({ seed: 42 });
await host.mount(<FollowerAlert />);
host.emit("twitch:follower", { user_name: "Alice" });
await host.settle();
expect(host.findByText("Alice")).toBeDefined();
});
host.emit(type, fields) starts with the backend's golden fixture, merges your fields, and validates the payload. Invalid types or fields fail before reaching the extension. host.play(raid({ viewers: 500 }), { instant: true }) plays a built-in scenario; scenarios also work with fake timers. host.chat(), host.redeem(), host.player, host.rewards, and host.viewers provide stateful input across tests. host.setTarget("layer") switches the next mount to a layer while keeping session state.
For bot-module and server-function integration, createBotRuntime() from @zaflun/lumio-devkit/node runs a built dist/server.js in the local V8 replica. It supports commands, events, timers, ctx.db, ctx.cache, ctx.defer, secrets from lumio/mocks/secrets.json, and realtime pushes to mounted surfaces. loadMockFiles() reads project presets and mock service data from lumio/mocks/. See Bot modules and Viewer input for end-to-end examples.
Exercising one dev session
Run lumio dev in the project root, open its sandbox URL, and use its Events, Chat, Rewards, Music, Scenarios, Data, and Server panels. The terminal REPL and a second terminal running lumio trigger operate on the same mock host. For repeatable event pressure, try:
lumio trigger twitch:follower --count 50 --rate 5/s
This validates local extension behavior. An installed extension is still needed to verify real platform connections, account permissions, persistent data, and the production runtime origin.
Manual testing checklist
Before submitting for review, work through this checklist using lumio dev:
Editor panel
- All config fields render correctly
- Changing a setting immediately updates the layer (no page reload needed)
- Default values display when config is empty
- Text inputs accept long strings without layout breakage
- Required fields show a validation hint if empty
Layer
- Renders correctly at 1920×1080
- Renders correctly at 1280×720 (test by resizing the preview browser window)
- Transparent backgrounds are truly transparent (check against a non-black background)
- Fonts load correctly (no fallback to system font)
- Animations play at smooth 60 fps
Event handling
- Displays correctly when no event has arrived yet (null state)
- Responds to the first event within 1 second
- Handles rapid events without visual glitches or memory leaks
- Auto-dismiss timers work correctly
Server functions
- Actions return expected results
- Error conditions are handled gracefully (no unhandled promise rejections in the console)
- External API failures show a user-friendly fallback
Release-candidate deployment
Deploy a release candidate and test it with real Twitch/YouTube events on your own channel before you submit it for review:
lumio deploy --version 0.9.0-rc1 --changelog "Release candidate for testing"
Install the extension on a test overlay and go live on Twitch. Trigger real events (follow, subscribe, chat) to verify end-to-end behavior.
Check production logs while testing:
lumio logs --tail --level info