System Testing ViewComponents
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.