Skip to main content

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​

LevelWhat to testTool
UnitPure functions, validators, data transformsVitest or Jest
ComponentRendering logic, UI state, config readsReact Testing Library
IntegrationSDK surfaces, events, actions, and server or bot handlers@zaflun/lumio-devkit
Manual (local)Full flow: editor → layer → eventslumio dev
Manual (live)Live overlay in browser source with real eventslumio 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