Concepts
Testing
Test components with Vitest: render into a DOM, interact, flush updates and assert, and test server rendering as HTML strings.
#Concept
Vitest uses your Vite config, so lumiose() compiles .stsx and .tsx test subjects the same way
as the app. Two things matter for component tests:
- A DOM environment. Use
happy-dom(orjsdom), per file with// @vitest-environment happy-domor for all tests withtest.environment. - The browser build of
lumiose.lumiosehas a browser entry (signals, the DOM renderer) and anodeentry (server rendering, where signals are plain values and effects never run). Vitest resolves Node conditions, butlumiose()gives tests in a DOM environment the browser entry oflumiose, so rendered components update. Tests without a DOM environment keep thenodeentry.
Updates are batched: after a write or an event outside a delegated handler, call flush()
before asserting. Delegated events (onClick) need the element to be attached to document.
Server rendering is tested with renderToString(() => <Page />) under the node entry: a test
file without a DOM environment, in the same project.
Scoped state is fresh in each test. lumiose() adds a Vitest setup file that resets
defineState instances before each test, so one test's cart never leaks into the next. In server
tests, a state's accessor called outside a render (to seed or inspect it) uses one instance per
test, which renderToString then shares.
Status:
@lumiose/testing(arender()helper and Testing Library integration) is planned. Until then, userenderfromlumiose/domas shown here.
#API
| API | Use |
|---|---|
render(() => <App />, container) from lumiose/dom |
Render into a DOM node; returns a disposer. |
flush() |
Run pending bindings and effects now. |
renderToString(() => <App />) |
Server-render to a string (node entry). |
#Example
// vitest.config.ts
import lumiose from "@lumiose/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [lumiose()],
test: { environment: "happy-dom" },
});// src/Counter.test.tsx
import { flush } from "lumiose";
import { render } from "lumiose/dom";
import { expect, it } from "vitest";
import { Counter } from "./Counter.stsx";
it("counts clicks", () => {
const container = document.body.appendChild(document.createElement("div"));
const dispose = render(() => <Counter start={2} />, container);
const button = container.querySelector("button")!;
button.click();
flush();
expect(button.textContent).toContain("Clicked 3 times");
dispose();
container.remove();
});#Common mistakes
- No DOM environment. Without
happy-dom(orjsdom) there is nodocument, andlumioseresolves to itsnodeentry, which has no reactive graph: set the environment for component tests. - Asserting right after a write. Effects and bindings run in a microtask; call
flush()first. - Clicking a detached element. Delegated handlers listen at
document; append the container todocument.body. - Not disposing. Call the disposer returned by
renderso effects and listeners from one test do not leak into the next. - Testing server components in a DOM environment. Async server components that use
request(),cookies()or*.server.tsmodules belong in server-side tests withrenderToString.
#Related diagnostics
- LUM4003: server-only module in a client graph (also
renderToStringcalled with the browser entry). - LUM4004: browser API at module scope.
See also: Getting started, reference: lumiose/dom, reference: server.