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.