Skip to content

Testing targets & classes

Required vs optional targets

Stimulus generates hasXTarget for every declared target. Use it to assert presence without throwing:

ts
const { controller } = await render(HelloController, {
  html: `
    <div data-controller="hello">
      <span data-hello-target="output"></span>
    </div>
  `,
})

expect(controller.hasOutputTarget).toBe(true)
expect(controller.hasNameTarget).toBe(false)

Accessing controller.nameTarget when none is present throws at runtime — by design. Guard with hasNameTarget or rely on declared optionality.

Multiple targets

xTargets (plural) returns every matching element:

ts
const { controller } = await render(TabsController, {
  html: `
    <div data-controller="tabs">
      <section data-tabs-target="panel">One</section>
      <section data-tabs-target="panel">Two</section>
    </div>
  `,
})

expect(controller.panelTargets).toHaveLength(2)
expect(controller.panelTargets[0].textContent).toBe('One')

Class bookkeeping

Stimulus exposes declared classes via this.xClass. A controller with:

js
static classes = ['open']

…reads the class name from data-<id>-open-class:

ts
await render(ToggleController, {
  html: `<div ${attr.controller('toggle', {}, { open: 'is-open' })}></div>`,
})

Assert class toggling by inspecting the DOM, not the controller internals:

ts
const { controller, user, element, getByRole } = await render(ToggleController, {
  html: `
    <div ${attr.combine(attr.controller('toggle', {}, { open: 'is-open' }))}>
      <button ${attr.combine(
        attr.target('toggle', 'trigger'),
        attr.action('toggle', 'toggle', 'click'),
      )}>Menu</button>
      <div ${attr.target('toggle', 'content')} hidden>Hidden</div>
    </div>
  `,
})

const trigger = getByRole('button', { name: 'Menu' })
await user.click(trigger)
expect(trigger.classList.contains('is-open')).toBe(true)

Released under the MIT License.