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
IntegrationServer function handlers with mocked storageVitest + hand-rolled mocks
Manual (local)Full flow: editor → layer → eventslumio dev
Manual (staging)Live overlay in OBS with real eventslumio 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