a small wrapper around DOM Testing Library for use with Remix v3
npm install --save-dev @mcansh/remix-testing-librarypnpm add --save-dev @mcansh/remix-testing-libraryyarn add --dev @mcansh/remix-testing-libraryYou will also need a DOM environment for your test runner. For example, with Vitest and happy-dom:
npm install --save-dev vitest happy-dom// vitest.config.js
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
environment: "happy-dom",
globals: true, // required for auto-cleanup after each test
},
})import { render, screen } from "@mcansh/remix-testing-library"
it("renders a greeting", () => {
function Greeting() {
return (props: { name: string }) => <p>Hello, {props.name}!</p>
}
render(<Greeting name="world" />)
screen.getByText("Hello, world!")
})import { render, screen } from "@mcansh/remix-testing-library"
it("updates when rerendered with new props", () => {
function Greeting() {
return (props: { message: string }) => <div>{props.message}</div>
}
const { rerender } = render(<Greeting message="hi" />)
screen.getByText("hi")
rerender(<Greeting message="hey" />)
screen.getByText("hey")
})Note: The interactive example below uses
@testing-library/user-event, which must be installed separately:npm install --save-dev @testing-library/user-event
import { on, pressEvents, type Handle } from "@remix-run/component"
import { userEvent } from "@testing-library/user-event"
import { render, screen } from "@mcansh/remix-testing-library"
it("increments a counter on click", async () => {
let user = userEvent.setup()
function Counter(handle: Handle) {
let count = 0
return () => (
<button
mix={[
pressEvents(),
on(pressEvents.press, async () => {
count += 1
await handle.update()
}),
]}
>
Clicked {count} time{count === 1 ? "" : "s"}
</button>
)
}
render(<Counter />)
const button = screen.getByRole("button")
expect(button.textContent).toBe("Clicked 0 times")
await user.click(button)
expect(button.textContent).toBe("Clicked 1 time")
})Renders a Remix component into a container appended to document.body and returns an object with:
| Property | Description |
|---|---|
container |
The div element the component was rendered into |
baseElement |
document.body (or the custom baseElement option if provided) |
debug(el?, maxLength?, options?) |
Logs a pretty-printed DOM snapshot to the console |
rerender(ui) |
Re-renders the component with new props |
dispose() |
Unmounts the component |
asFragment() |
Returns a DocumentFragment snapshot of the current DOM |
...queries |
All DOM Testing Library queries bound to baseElement |
Options:
| Option | Type | Description |
|---|---|---|
container |
HTMLElement |
Custom container element (defaults to a new div appended to baseElement) |
baseElement |
HTMLElement |
Custom base element for queries (defaults to document.body) |
wrapper |
component | Wraps the rendered UI with the given component |
queries |
Queries |
Custom queries to bind to baseElement (defaults to all @testing-library/dom queries) |
virtualRootOptions |
VirtualRootOptions |
Options forwarded to createRoot from @remix-run/component |
Unmounts all components rendered with render and removes their containers from document.body. This is called automatically after each test only when a global afterEach or teardown function is available on globalThis. Jest provides this by default; in Vitest, you must enable test.globals: true in your Vitest config — simply importing afterEach in a test file is not sufficient. Import from @mcansh/remix-testing-library/pure to opt out of auto-cleanup.
If you prefer to manage cleanup yourself, import from the pure entry point:
import { render, cleanup } from "@mcansh/remix-testing-library/pure"You can also disable auto-cleanup globally by setting the RTL_SKIP_AUTO_CLEANUP environment variable to "true".
@mcansh/remix-testing-library re-exports everything from @testing-library/dom, including all queries (e.g. screen, getByText, findByRole) and utilities (e.g. waitFor, fireEvent).