StimulusSpec

CI Gem Version Gem Downloads Ruby codecov

Drop-in RSpec matchers for hotwired/stimulus-rails — stop hand-rolling data-controller assertions and test your Stimulus wiring with expressive, purpose-built matchers.

  • Request/controller specshave_stimulus_controller, have_stimulus_action, have_stimulus_target, have_stimulus_value, have_stimulus_class, have_stimulus_outlet
  • System/feature specs — Capybara matchers: have_stimulus_controller, have_stimulus_action, have_stimulus_target, have_stimulus_value, have_stimulus_class, have_stimulus_outlet
  • Auto-included — zero setup required when stimulus-rails is in your bundle
  • Configurable — disable auto-include when you need manual control

Companion gem to turbo_rspec — together they cover the full Hotwire testing stack.

Table of Contents

Installation

Add to your application's Gemfile:

group :test do
  gem "stimulus_spec"
end

Back to top

Setup

Rails + stimulus-rails (automatic)

No setup needed. When stimulus-rails is in your bundle:

  • StimulusSpec::Matchers is automatically included in type: :request and type: :controller example groups
  • StimulusSpec::Capybara::Matchers is automatically included in type: :system and type: :feature example groups when capybara is also present

Manual include

For non-Rails projects or custom contexts, include the matchers explicitly:

# spec/spec_helper.rb
RSpec.configure do |config|
  config.include StimulusSpec::Matchers                 # request/controller specs
  config.include StimulusSpec::Capybara::Matchers       # system/feature specs
end

Configuration

# spec/support/stimulus_spec.rb
StimulusSpec.configure do |config|
  config.auto_include = false  # disable automatic inclusion
end

Back to top

Matchers

have_stimulus_controller

Assert that rendered HTML contains a data-controller attribute with the given controller name.

expect(response).to have_stimulus_controller("hello")

# Assert multiple controllers on a single element
expect(response).to have_stimulus_controller("hello", "clipboard")

# Negation
expect(response).not_to have_stimulus_controller("missing")

Uses space-separated token matching (~=), so it works correctly when multiple controllers are declared on a single element and won't partially match.

have_stimulus_action

Assert that rendered HTML contains a data-action attribute with the given action descriptor.

# Full descriptor
expect(response).to have_stimulus_action("click->hello#greet")

# Shorthand — matches any event
expect(response).to have_stimulus_action("hello#greet")

# Negation
expect(response).not_to have_stimulus_action("hello#disconnect")

have_stimulus_target

Assert that rendered HTML contains a data-{controller}-target attribute with the given target name.

expect(response).to have_stimulus_target("hello", "name")
expect(response).to have_stimulus_target("hello", "output")

# Negation
expect(response).not_to have_stimulus_target("hello", "missing")

have_stimulus_value

Assert that rendered HTML contains a data-{controller}-{name}-value attribute, optionally with a specific value.

# Assert the value attribute exists
expect(response).to have_stimulus_value("search", "url")

# Assert a specific value
expect(response).to have_stimulus_value("search", "url", "/results")

# Negation
expect(response).not_to have_stimulus_value("search", "url")

have_stimulus_class

Assert that rendered HTML contains a data-{controller}-{name}-class attribute, optionally with a specific class.

# Assert the class attribute exists
expect(response).to have_stimulus_class("search", "loading")

# Assert a specific class value
expect(response).to have_stimulus_class("search", "loading", "opacity-50")

# Negation
expect(response).not_to have_stimulus_class("search", "loading")

have_stimulus_outlet

Assert that rendered HTML contains a data-{controller}-{outlet}-outlet attribute with a CSS selector.

# Assert the outlet attribute exists
expect(response).to have_stimulus_outlet("search", "results")

# Assert a specific selector
expect(response).to have_stimulus_outlet("search", "results", "#results-list")

# Negation
expect(response).not_to have_stimulus_outlet("search", "results")

Scoped matching with .within

All matchers support .within(selector) to restrict matching to a specific part of the page:

# Request/controller specs — search within a CSS selector
expect(response).to have_stimulus_controller("search").within(".search-form")
expect(response).to have_stimulus_action("click->search#query").within(".search-form")
expect(response).to have_stimulus_target("search", "input").within(".search-form")

# System/feature specs — same API
expect(page).to have_stimulus_controller("search").within(".search-form")

Back to top

Example

RSpec.describe "Search", type: :request do
  describe "GET /search" do
    it "wires up the search controller" do
      get search_path

      expect(response).to have_stimulus_controller("search")
      expect(response).to have_stimulus_action("input->search#query")
      expect(response).to have_stimulus_target("search", "input")
    end
  end
end

Example: system spec

RSpec.describe "Search", type: :system do
  it "has the search controller wired up" do
    visit search_path

    expect(page).to have_stimulus_controller("search")
    expect(page).to have_stimulus_action("input->search#query")
    expect(page).to have_stimulus_target("search", "input")
  end
end

Back to top

Relationship to turbo_rspec

turbo_rspec includes basic Stimulus matchers (have_stimulus_controller, have_stimulus_action, have_stimulus_target). stimulus_spec goes deeper with value, class, and outlet matchers, plus richer failure messages and Stimulus-specific configuration. If you only need basic controller/action/target assertions alongside your Turbo matchers, turbo_rspec has you covered. If you want comprehensive Stimulus testing, use stimulus_spec.

Both gems can coexist — they use separate namespaces and won't conflict.

Back to top

Contributing

Bug reports and pull requests are welcome on GitHub. See CONTRIBUTING.md for setup instructions, branch conventions, CHANGELOG requirements, and the PR checklist.

License

The gem is available as open source under the MIT License.

Back to top