Skip to content

Getting Started ​

This page walks you from a blank project to a green test in under five minutes.

Requirements ​

RequirementVersion
Node.js18.x, 20.x, or 22.x
@hotwired/stimulus^3.2 (peer dependency)
Test runnerVitest ^2 (recommended) or any runner with an afterEach hook
DOMhappy-dom (recommended) or jsdom

1. Install ​

bash
npm install -D @tito10047/stimulus-test-utils @hotwired/stimulus vitest happy-dom

@hotwired/stimulus is declared as a peer dependency. Install the same version your application ships — the harness will use it directly.

2. Configure Vitest ​

Enable a DOM environment and register the cleanup hook:

ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'happy-dom',
    setupFiles: ['@tito10047/stimulus-test-utils/register'],
  },
})

The /register side-effect module calls afterEach(cleanup) for you.

If you prefer explicit wiring, skip setupFiles and do it yourself:

ts
// tests/setup.ts
import { afterEach } from 'vitest'
import { cleanup } from '@tito10047/stimulus-test-utils'
afterEach(cleanup)

3. Write your first test ​

src/hello_controller.js:

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}!`
  }
}

tests/hello.test.js:

js
import { render, attr.controller, attr.target, attr.action } from '@tito10047/stimulus-test-utils'
import { expect, test } from 'vitest'
import HelloController from '../src/hello_controller.js'

test('greets by name', async () => {
  const { controller, user, element, 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!')
})

4. Run it ​

bash
npx vitest run

You should see a single passing test. If the test fails with greetingValue being undefined or a target not found, jump to Troubleshooting.

What just happened ​

render() performed these steps, in this order:

  1. Created a new Stimulus Application (a fresh one per test — see Cleanup & isolation).
  2. Registered HelloController under the identifier "hello", inferred from the class name.
  3. Parsed the html fixture and appended it to document.body.
  4. Awaited the MutationObserver tick so Stimulus' connect() lifecycle fires.
  5. Resolved with the controller instance and a suite of helpers scoped to the mounted element.

Next steps ​

Released under the MIT License.