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 | Server function handlers with mocked storage | Vitest + hand-rolled mocks |
| Manual (local) | Full flow: editor → layer → events | lumio dev |
| Manual (staging) | Live overlay in OBS with real events | lumio deploy to staging |
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
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();
});
});
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
OBS 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
Interactive page
- Renders on mobile screen sizes (375px wide)
- Unauthenticated users see an appropriate message or login prompt
- All user interactions are reflected on the layer within 500ms
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
Staging deployment
Deploy to staging to test with real Twitch/YouTube events before submitting:
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