By combining ViewComponent with Stimulus, you now have an easy way to componentize your view (HTML) together with its logic (JavaScript). Testing these together may appear challenging at first glance, as component tests typically test only against the output of what a ViewComponent renders. This article aims to show how to combine ViewComponent previews with Rails system tests as a way to test the interactivity of your ViewComponents in isolation from the rest of your application.

Our ViewComponent

We will be working with a Counter ViewComponent. I chose this example because what it renders is fairly lightweight, without too much to test, but it includes a Stimulus controller, and this is where our approach to system testing ViewComponent previews can shine.

The component takes a starting value which can be used to override the default starting value of 0.

# app/components/counter/component.rb
class Counter::Component < ViewComponent::Base

  def initialize(starting: 0)
    @starting = starting
  end

end

This component renders HTML that includes the starting value along with two buttons to increment and decrement that value.

<!-- app/components/counter/component.html.erb -->
<div data-controller="counter--component">
  <h2>Count: <span data-counter--component-target="value"><%= @starting %></span></h2>
  <button aria-label="Increment" data-action="click->counter--component#increment">+</button>
  <button aria-label="Decrement" data-action="click->counter--component#decrement">-</button>
</div>

As mentioned, this ViewComponent is paired with a Stimulus controller to provide the client-side interactivity of incrementing and decrementing the starting value.

// app/components/counter/component_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["value"]

  increment() {
    this.valueTarget.innerText = parseInt(this.valueTarget.innerText) + 1;
  }

  decrement() {
    this.valueTarget.innerText = parseInt(this.valueTarget.innerText) - 1;
  }
}

Testing ViewComponent Output

One advantage of using ViewComponents over partials is that they can be easily tested in isolation. There isn’t the need to render an entire view when you are just interested in a specific partial’s output. Here we are ensuring that our ViewComponent correctly renders the starting value inside of an h2 tag.

# spec/components/counter/component_spec.rb
RSpec.describe Counter::Component, type: :component do
  it "renders with default count" do
    component = described_class.new(starting: 5)
    render_inline(component)
    expect(page).to have_css("h2", text: "Count: 5")
  end
end

Testing ViewComponent Interactivity

The reality of our Counter component is that what it renders is fairly lightweight, and what is really important to us is that the interactivity (via Stimulus) works as expected.

One approach is to create a system test that renders a page that just happens to include the ViewComponent we are interested in testing. The drawback here is that we often have to log in as the correct user, ensure the context and data that this page requires is set up correctly, and because of this is can be painful to write and slow to execute.

ViewComponent comes with the ability to create previews which render your ViewComponent in isolation on its own page. At Wrapbook we make heavy use of previews for our design system tool Lookbook.

# spec/components/previews/counter/component_preview.rb
class Counter::ComponentPreview < ViewComponent::Preview

  def default
    render Counter::Component.new(starting: 5)
  end

end

This preview generates a URL that can be visited in the browser at http://localhost:3000/rails/view_components/counter/component/default. Now that we have a URL, it also means we can write a system test against this URL, giving us the ability to test the interactivity of our ViewComponent in a browser, in isolation.

# spec/system/components/counter/component_spec.rb
RSpec.describe "Counter::Component" do
  before { visit "/rails/view_components/counter/component/default" }

  context "counter" do
    it "increments" do
      expect(page).to have_selector("h2", text: "Count: 5")
      find('[aria-label="Increment"]').click
      expect(page).to have_selector("h2", text: "Count: 6")
    end

    it "decrements" do
      expect(page).to have_selector("h2", text: "Count: 5")
      find('[aria-label="Decrement"]').click
      expect(page).to have_selector("h2", text: "Count: 4")
    end
  end
end

Conclusion

By combining ViewComponent previews with Rails system testing, we have a more performant and isolated approach to test the interactivity of our ViewComponent. This is especially useful when a ViewComponent encapsulates functionality provided by a Stimulus controller.