StimulusReflex Testing

🚨🚨🚨🚨🚨🚨 We are no longer maintining this gem. 🚨🚨🚨🚨🚨🚨

If you're interested in taking ownership of this project, please reach out. Otherwise we'll likely archive it soon.

Installation

Add this line to your application's Gemfile:

gem 'stimulus_reflex_testing'

Using 5.2 or below? Or using a version of RSpec Rails lower than 4?

Both Rails 6 and RSpec Rails 4 introduce the action-cable-testing library. If you're using Rails 5.2 and a version of RSpec Rails lower than 4, include the action-cable-testing gem in your Gemfile.

gem 'stimulus_reflex_testing'
gem 'action-cable-testing'

And then execute:

$ bundle install

RSpec instructions

In spec/rails_helper.rb make sure to add: require "stimulus_reflex_testing/rspec"

Rspec tests include the reflex testing functionality when type: :reflex is provided. If this type is not provided, your Reflex tests won't work.

Usage

build_reflex

To build an instance of your reflex with the required setup handled (channel, url, element, etc) you can use the build_reflex method.

Note: In previous versions of the library you didn't have to provide the action you want to test in build_reflex. You would build the reflex and then provide the action to the run method. Now, the "proper" way to do this is to pass the action as method_name to build_reflex.

# With a valid URL so StimulusReflex can build the correct request
build_reflex(method_name: :create, url: posts_url)

# Does your test rely on a `current_user` (or similar) defined in `ApplicationCable::Connection`?
build_reflex(method_name: :create, url: posts_url, connection: { current_user: create(:user) })

# Do you rely on form params?
build_reflex(method_name: :create, url: edit_post_url(post), params: { post: { title: 'A new title!' } })

# Need to give your element some data?
reflex = build_reflex(method_name: :create, url: posts_url)
reflex.element.value = "Hello"
reflex.element.dataset.id = "123"

#run

To unit test a reflex, we provide a method (when using build_reflex) called #run. This method takes a method name, and any arguments.

Note: The method name is now optional if you provide the action to build_reflex, which is now the "correct" way setup your reflex test.

# Providing the action during setup
reflex = build_reflex(method_name: :create, url: posts_url)
reflex.run

# Providing the action during setup with arguments
reflex = build_reflex(method_name: :create, url: posts_url)
reflex.run(nil, arg1, arg2)

# Legacy: Providing the action at "runtime" with arguments
reflex = build_reflex(url: posts_url)
reflex.run(:create, arg1, arg2)

Why does the action need to be in build_reflex now?

If we wait don't provide the Reflex action until after we've built the reflex, the callbacks do not setup correctly. If you're using callbacks (especially with :only or :except) you may run into issues if you don't provide the action until calling #run.

Why can't I just call the method directly?

You're more than welcome to call the method directly. But, be advised you will lose callbacks. If you need to run before_reflex (etc) use the run method.

#run is a wrapper around the underlying #process method in StimulusReflex that runs callbacks.

#get

To grab an instance variable set by a reflex, you can use the #get method when using a reflex built with build_reflex.

# app/reflexes/post_reflex.rb
class PostReflex < ApplicationReflex
  def find_post
    @post = Post.find(params[:id])
  end
end

reflex = build_reflex(method_name: :find_post, url: edit_post_url(post), params: { id: post.id })
reflex.run
reflex.get(:post) #=> returns the @post instance variable

Matchers

morph(selector)

You can assert that a "run" reflex morphed as you expected:

# app/reflexes/post_reflex.rb
class PostReflex < ApplicationReflex
  def delete
    @post = Post.find(params[:id])
    @post.destroy
    morph dom_id(@post), ""
  end
end

# spec/reflexes/post_reflex_spec.rb
require 'rails_helper'

RSpec.describe PostReflex, type: :reflex do
  let(:post) { create(:post) }
  let(:reflex) { build_reflex(method_name: :delete) }

  describe '#delete' do
    it 'morphs the post' do
      subject = reflex.run
      expect(subject).to morph("#post_#{post.id}")
    end
  end
end

You can also assert the content you provided to morph:

# spec/reflexes/post_reflex_spec.rb
require 'rails_helper'

RSpec.describe PostReflex, type: :reflex do
  let(:post) { create(:post) }
  let(:reflex) { build_reflex(method_name: :delete) }

  describe '#delete' do
    it 'morphs the post with an empty string' do
      subject = reflex.run
      expect(subject).to morph("#post_#{post.id}").with("")
    end
  end
end

You can also run the expecation as a block:

# spec/reflexes/post_reflex_spec.rb
require 'rails_helper'

RSpec.describe PostReflex, type: :reflex do
  let(:post) { create(:post) }
  let(:reflex) { build_reflex(method_name: :delete) }

  describe '#delete' do
    it 'morphs the post with an empty string' do
      expect { reflex.run }.to morph("#post_#{post.id}").with("")
    end
  end
end

broadcast(operations) (Experimental)

You can assert that a reflex will perform the CableReady operations you anticipate:

# app/reflexes/post_reflex.rb
class PostReflex < ApplicationReflex
  def delete
    @post = Post.find(params[:id])
    @post.destroy
    cable_ready[PostsChannel].remove(selector: dom_id(@post)).broadcast
  end
end

# spec/reflexes/post_reflex_spec.rb
require 'rails_helper'

RSpec.describe PostReflex, type: :reflex do
  let(:post) { create(:post) }
  let(:reflex) { build_reflex(method_name: :delete) }

  describe '#delete' do
    it 'broadcasts the CableReady operations' do
      expect { reflex }.to broadcast(:remove, :broadcast)
    end

    it 'removes the post' do
      expect { reflex }.to broadcast(remove: { selector: "#post_#{post.id}" }, broadcast: nil)
    end
  end
end

RSpec example

# app/reflexes/post_reflex.rb
class PostReflex < ApplicationReflex
  def validate
    @post = Post.find(element.dataset.post_id)
    @post.validate
  end
end

# spec/reflexes/post_reflex_spec.rb
require 'rails_helper'

RSpec.describe PostReflex, type: :reflex do
  let(:post) { create(:post) }
  let(:reflex) { build_reflex(method_name: :validate, url: edit_post_url(post)) }

  describe '#validate' do
    subject { reflex.run }

    before do
      reflex.element.dataset.post_id = post.id
      subject
    end

    it 'finds the post' do
      expect(reflex.get(:post)).to eq(post)
    end

    it 'validates the post' do
      expect(reflex.get(:post).errors).to be_present
    end
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/podia/stimulus_reflex_testing. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

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

Code of Conduct

Everyone interacting in the StimulusReflexTesting project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.