solid-impl-testing
Quick Reference
Installation
npm install --save-dev @solidjs/testing-library @testing-library/jest-dom vitest
npm install --save-dev vite-plugin-solid
Key Dependencies
| Package | Role |
|---|---|
@solidjs/testing-library |
SolidJS-specific render, cleanup, renderHook, testEffect |
@testing-library/dom |
Re-exported: screen, fireEvent, waitFor, within, queries |
@testing-library/jest-dom |
DOM matchers (toBeInTheDocument, toHaveTextContent) |
vitest |
Test runner (recommended over Jest for SolidJS) |
vite-plugin-solid |
Compiles JSX/TSX for tests (v2.8.2+ handles test config automatically) |
Vitest Configuration
// vite.config.ts
import { defineConfig } from "vitest/config";
import solidPlugin from "vite-plugin-solid";
export default defineConfig({
plugins: [solidPlugin()],
resolve: {
conditions: ["development", "browser"],
},
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test-setup.ts"],
deps: {
optimizer: {
web: {
include: ["solid-js", "@solidjs/router"],
},
},
},
},
});
// src/test-setup.ts
import "@testing-library/jest-dom";
Critical Warnings
ALWAYS pass a function returning JSX to render() -- NEVER pass JSX directly. SolidJS render() requires () => <Component />, not <Component />. Passing JSX directly executes it outside the test's reactive owner and breaks cleanup.
ALWAYS call cleanup() in afterEach -- even though the library claims auto-cleanup, explicitly calling it prevents disposal errors across test suites. Omitting cleanup causes reactive owners to leak between tests.
NEVER use rerender() -- it does NOT exist in solid-testing-library. SolidJS does not re-render; it executes side effects from reactive state changes. Use signals to drive state changes in tests instead.
NEVER destructure props in test components -- this breaks reactivity tracking. Use props.value access patterns, same as production components.
ALWAYS set resolve.conditions: ["development", "browser"] in Vitest config -- without this, Solid loads the server bundle in tests, causing hydration errors and missing DOM APIs.
Decision Trees
Which Testing API to Use
Need to test a component?
├─ Yes → render(() => <Component />) + screen queries
│ ├─ Component uses router? → Add { location: "/path" } option
│ ├─ Component uses context? → Add { wrapper: Provider } option
│ └─ Component has async data? → Use findBy* queries + waitFor
│
├─ Need to test a custom hook?
│ └─ Yes → renderHook(useMyHook) → access result directly
│
├─ Need to test a custom directive?
│ └─ Yes → renderDirective(myDirective) → use setArg for updates
│
└─ Need to test reactive effects?
└─ Yes → testEffect(done => createEffect(() => { ... done() }))
Which Query to Use
Can the element be found by its accessible role?
├─ Yes → getByRole("button", { name: "Submit" }) ← PREFERRED
│
├─ Is it a form field with a label?
│ └─ Yes → getByLabelText("Email")
│
├─ Does it have visible text content?
│ └─ Yes → getByText("Hello World")
│
├─ Is the element absent and you want to assert that?
│ └─ Yes → queryByText("Missing") → expect to be null
│
├─ Does the element appear asynchronously?
│ └─ Yes → findByText("Loaded Data") ← returns Promise
│
└─ No semantic way to find it?
└─ Use getByTestId("my-element") ← LAST RESORT
Core Patterns
Pattern 1: Basic Component Test
// Counter.test.tsx
import { render, screen, cleanup } from "@solidjs/testing-library";
import { fireEvent } from "@testing-library/dom";
import { afterEach, describe, expect, it } from "vitest";
import { Counter } from "./Counter";
afterEach(() => cleanup());
describe("Counter", () => {
it("increments on click", async () => {
render(() => <Counter />);
const button = screen.getByRole("button", { name: "Increment" });
expect(screen.getByText("Count: 0")).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
});
Pattern 2: Testing with Props via Signals
import { createSignal } from "solid-js";
import { render, screen, cleanup } from "@solidjs/testing-library";
import { afterEach, expect, it } from "vitest";
import { Greeting } from "./Greeting";
afterEach(() => cleanup());
it("reacts to prop changes", () => {
const [name, setName] = createSignal("Alice");
// Pass signal getter as prop -- component updates reactively
render(() => <Greeting name={name()} />);
expect(screen.getByText("Hello, Alice")).toBeInTheDocument();
setName("Bob");
expect(screen.getByText("Hello, Bob")).toBeInTheDocument();
});
Pattern 3: Testing with Context Providers
import { render, screen, cleanup } from "@solidjs/testing-library";
import { afterEach, expect, it } from "vitest";
import { ThemeProvider } from "./ThemeContext";
import { ThemedButton } from "./ThemedButton";
afterEach(() => cleanup());
it("uses theme from context", () => {
render(() => <ThemedButton />, {
wrapper: (props) => (
<ThemeProvider theme="dark">{props.children}</ThemeProvider>
),
});
expect(screen.getByRole("button")).toHaveClass("dark-theme");
});
Pattern 4: Testing Async Components
import { render, screen, cleanup } from "@solidjs/testing-library";
import { waitFor } from "@testing-library/dom";
import { afterEach, expect, it } from "vitest";
import { UserProfile } from "./UserProfile";
afterEach(() => cleanup());
it("loads and displays user data", async () => {
render(() => <UserProfile userId="123" />);
// Suspense fallback shows first
expect(screen.getByText("Loading...")).toBeInTheDocument();
// Wait for async data to resolve
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
});
Pattern 5: Testing Custom Hooks
import { renderHook, cleanup } from "@solidjs/testing-library";
import { afterEach, expect, it } from "vitest";
import { useCounter } from "./useCounter";
afterEach(() => cleanup());
it("returns count and increment", () => {
const { result } = renderHook(() => useCounter());
expect(result.count()).toBe(0);
result.increment();
expect(result.count()).toBe(1);
});
Pattern 6: Testing with Router
import { render, screen, cleanup } from "@solidjs/testing-library";
import { afterEach, expect, it } from "vitest";
import { App } from "./App";
afterEach(() => cleanup());
it("renders the correct route", async () => {
render(() => <App />, { location: "/users/42" });
// Router setup is async -- use findBy queries
const heading = await screen.findByText("User 42");
expect(heading).toBeInTheDocument();
});
Pattern 7: Testing Reactive Effects with testEffect
import { createSignal, createEffect } from "solid-js";
import { testEffect, cleanup } from "@solidjs/testing-library";
import { afterEach, expect, it } from "vitest";
afterEach(() => cleanup());
it("tracks signal changes in effects", () => {
const [count, setCount] = createSignal(0);
return testEffect((done) =>
createEffect((run: number = 0) => {
if (run === 0) {
expect(count()).toBe(0);
setCount(1);
} else if (run === 1) {
expect(count()).toBe(1);
done();
}
return run + 1;
})
);
});
File Organization
src/
├── components/
│ ├── Counter.tsx
│ └── Counter.test.tsx ← Co-located test files
├── hooks/
│ ├── useCounter.ts
│ └── useCounter.test.ts
├── test-setup.ts ← Global test setup
└── __tests__/ ← Alternative: dedicated test folder
└── integration/
└── app.test.tsx
ALWAYS use .test.tsx extension for files containing JSX in tests. Use .test.ts only for pure logic tests without JSX.
Reference Links
- references/methods.md -- API signatures for render, screen, fireEvent, cleanup, waitFor, renderHook, testEffect
- references/examples.md -- Complete test examples for components, signals, stores, async, and events
- references/anti-patterns.md -- React Testing Library habits that break SolidJS tests