.claude/skills/solidjs/solid-impl/solid-impl-testing/SKILL.md
Use when writing unit or integration tests for SolidJS components, signals, or stores. Prevents missing cleanup calls, incorrect async test patterns, and React Testing Library habits that break SolidJS tests. Covers @solidjs/testing-library render, screen queries, fireEvent, cleanup, async testing, renderHook, testEffect, and vitest configuration. Keywords: @solidjs/testing-library, vitest, render, fireEvent, screen, cleanup, renderHook, testEffect, component testing.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer solid-impl-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
npm install --save-dev @solidjs/testing-library @testing-library/jest-dom vitest
npm install --save-dev vite-plugin-solid
| 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) |
// 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";
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.
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() }))
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
// 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();
});
});
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();
});
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");
});
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();
});
});
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);
});
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();
});
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;
})
);
});
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.
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.