Setting up React Testing Library
Component tests with React Testing Library on Bun's test runner, using Happy DOM as the DOM environment (not jsdom).
Prerequisites
- Bun project with React
tsconfig.jsonalready includes DOM libs ("lib": ["ESNext", "DOM", "DOM.Iterable"]) — see the TypeScript guide for the browser-app config
Steps
Install
bun add -d @happy-dom/global-registrator bun add -d @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event@happy-dom/global-registratorinjects browser APIs (document,window, …) into the test process@testing-library/jest-domadds matchers liketoBeInTheDocument@testing-library/user-eventis the interaction helper — prefer it overfireEvent
Create the Happy DOM preload
- This must run before any
@testing-library/*import, so keep it in its own file
// src/happydom.ts import { GlobalRegistrator } from "@happy-dom/global-registrator"; GlobalRegistrator.register();- This must run before any
Create the Testing Library preload
- Extends Bun's
expectwith jest-dom matchers, and unmountsrenderafter each test so leftover DOM doesn't leak between cases
// src/testing-library.ts import { afterEach, expect } from "bun:test"; import { cleanup } from "@testing-library/react"; import * as matchers from "@testing-library/jest-dom/matchers"; expect.extend(matchers); afterEach(() => { cleanup(); });- Extends Bun's
Register both preloads in
bunfig.toml- Order matters: Happy DOM first, Testing Library second
# bunfig.toml [test] preload = ["./src/happydom.ts", "./src/testing-library.ts"]Add matcher types
- Declaration merging so
toBeInTheDocumentand the other jest-dom matchers type-check onexpect(...). Lives undersrc/so the browser-apptsconfig.json"include"picks it up
// src/matchers.d.ts import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers"; import type { Matchers, AsymmetricMatchers } from "bun:test"; declare module "bun:test" { interface Matchers<T> extends TestingLibraryMatchers<typeof expect.stringContaining, void> {} interface AsymmetricMatchers extends TestingLibraryMatchers<any, any> {} }- Declaration merging so
Write a smoke test
- Colocate tests next to the file they cover (
*.test.tsxfor components). Query by role or label —getByTestIdis a last resort
// src/components/greeting.test.tsx import { test, expect } from "bun:test"; import { render, screen } from "@testing-library/react"; function Greeting({ name }: { name: string }) { return <h1>Hello, {name}</h1>; } test("renders the name in a heading", () => { render(<Greeting name="Ada" />); expect(screen.getByRole("heading", { name: "Hello, Ada" })).toBeInTheDocument(); });- For clicks and typing, import
userEventfrom@testing-library/user-eventand calluserEvent.setup()inside each test so instances don't share event state
- Colocate tests next to the file they cover (
Verification
-
bun testruns the smoke test and it passes -
expect(...).toBeInTheDocumenttype-checks in the editor (no red squiggle on the matcher)
References
- Using Testing Library with Bun — official setup this guide is based on
- Happy DOM with Bun — DOM environment only, no Testing Library
- DOM testing — Bun's DOM testing overview
- Testing Library docs — queries,
userEvent, and async utilities - Happy DOM repo