Zero‑config, Testing‑Library‑flavoured test harness for Stimulus controllers.
Write your tests in plain JavaScript or TypeScript, mount a controller with a single call, simulate user interactions, and assert against the DOM / controller state — without ever touching happy‑dom/JSDOM, the Stimulus Application, or MutationObserver timing by hand.
Testing Stimulus controllers usually means:
- setting
document.body.innerHTML, - creating and starting an
Application, - registering the controller,
- waiting for
connect()viaMutationObserverorawait nextTick(), - cleaning up after every test.
This library hides all of that behind a single render() call and exposes a familiar Testing Library-style API (getByRole, findByText, user.click, …).
npm install -D @tito10047/stimulus-test-utils @hotwired/stimulus vitest happy-dom@hotwired/stimulus is a peer dependency — you bring the version your app uses.
vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom',
setupFiles: ['@tito10047/stimulus-test-utils/register'],
},
})The /register module wires up afterEach(cleanup) automatically. If you prefer to clean up manually, omit setupFiles and call cleanup() yourself.
Controller:
// hello_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['name', 'output']
static values = { greeting: { type: String, default: 'Hello' } }
greet() {
this.outputTarget.textContent = `${this.greetingValue}, ${this.nameTarget.value}!`
}
}Test:
import { render, attr } from '@tito10047/stimulus-test-utils'
import { expect, test } from 'vitest'
import HelloController from './hello_controller.js'
test('greets by name', async () => {
const { element, controller, user, getByRole } = await render(HelloController, {
html: `
<div ${attr.controller('hello', { greeting: 'Hi' })}>
<input ${attr.target('hello', 'name')} />
<button ${attr.action('hello', 'greet', 'click')}>Greet</button>
<span ${attr.target('hello', 'output')}></span>
</div>
`,
})
await user.type(element.querySelector('input'), 'Ada')
await user.click(getByRole('button', { name: 'Greet' }))
expect(controller.outputTarget.textContent).toBe('Hi, Ada!')
})No Application.start(), no document.body.innerHTML = …, no manual await nextTick().
render(ControllerClass, options)— mounts the fixture, starts anApplication, registers the controller and waits forconnect().- Query helpers —
getByRole,getByText,getByTestId,findBy*,queryBy*,getAllBy*scoped to the mounted root. user— user‑event simulations:click,type,keyboard,hover, …waitFor/nextTick— async assertions for reactive DOM changes.- Attribute helpers —
attr.controller,attr.target,attr.action,attr.combineproduce safe, typo‑freedata-*attributes. cleanup()— automatically stops theApplicationand removes the fixture (via the/registersetup, or called manually).
A complete API overview is available in public_api.md and on the documentation site (see Documentation below).
- Node.js
18.x,20.x,22.x @hotwired/stimulus^3.2- Vitest
^2
npm ci
npm test # vitest in watch mode
npm run typecheck # tsc --noEmit
npm run build # tsup -> dist/Pull requests are welcome. Before opening a PR, please run npm run typecheck and npx vitest run.
The full documentation lives in docs/ and is published at https://tito10047.github.io/stimulus-test-utils/.
It is built with VitePress (prose + navigation) and TypeDoc with typedoc-plugin-markdown (auto‑generated API reference from TSDoc comments in src/).
npm ci
npm run docs:dev # start dev server on http://localhost:5173This generates the API reference into docs/api/generated/ (gitignored) and runs the VitePress dev server with hot‑reload. Edit any .md file under docs/ and see the change instantly.
npm run docs:build # runs docs:api, then vitepress build
npm run docs:preview # serve the production build locallyThe static site is emitted to docs/.vitepress/dist/.
Deployment is fully automated via .github/workflows/docs.yml:
- Trigger: every push to
main, or a manual run from the Actions tab (workflow_dispatch). - Build:
npm ci→npm run docs:build→ uploaddocs/.vitepress/distas a Pages artifact. - Deploy:
actions/deploy-pages@v4publishes it.
One-time repository setup (only needed once):
- Go to Settings → Pages.
- Set Source to GitHub Actions.
- Push to
main(or trigger the workflow manually). The first run will populate the URL shown above.
If you fork the repository, update the
baseoption indocs/.vitepress/config.tsto match your repository name (for example/my-fork/), and tweak the GitHub link in the same file.
MIT © tito10047