Testing ViewComponent w/ Capybara

Alvin CrespoAlvin Crespo
6 min read

Today I was writing some tests and dug into a little bit of ViewComponent and Capybara. Most of time you read a tutorial and just kind of get whatever done, here I'm going to explain in detail what my test is doing and why it's important.

๐Ÿ’ก
If you're using the Trix editor, I also show you how to test your view components with a nice helper inspired by Will Olson's article Testing the Trix Editor with Capybara and MiniTest.

Setup

Here you'll see the component, it's template and associated test. This component is responsible for rendering out a form that can be used in multiple parts of an application; including /notes, garden/:id/notes and /garden/:garden_id/plants/:plant_id/notes. To support this usage, we pass in form_options to the component that then gets used by the form_with view helper.

To keep this article simple, we're just going to verify the form renders the title, summary and body of a note.

Component

# app/components/note_form_component.rb

# frozen_string_literal: true

class NoteFormComponent < ViewComponent::Base
  delegate :rich_text_area_tag, to: :helpers

  def initialize(form_options:)
    super

    @form_options = form_options
  end
end

Template

<%# app/components/note_form_component.html.erb  %>

<%= form_with(**@form_options) do |form| %>
  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title, autofocus: :true, class: "input" %>
  </div>

  <div class="field">
    <%= form.label :summary %>
    <%= form.text_area :summary %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.rich_text_area :body %>
  </div>

  <div class="flex flex-row-reverse items-center mt-8">
    <%= form.submit %>
    <%= link_to "Cancel Note", :back, class: "mr-4" %>
  </div>
<% end %>

Test

# test/components/note_form_component_test.rb

require 'test_helper'

class NoteFormComponentTest < ViewComponent::TestCase
  def form_options
    { form_options: { model: Note.new, url: '/notes', local: true } }
  end

  def test_component_renders_form_with_title_field
      # assert the hell out of this
  end
end

ViewComponent + Capybara

The ViewComponent docs mention that we can use Capybara in our specs and I want specifically want to use assert_selector to verify the contents of my components are rendered.

๐Ÿ“•
assert_selector is a "Matcher" in Capybara. It asserts that an element is found at lease once.

To verify that all my elements are within a "form", I'm going to use assert_selector with a block and then verify all my elements are within it. This is done like so:


  assert_selector 'form' do
      # assert a form field
  end

Let's start with the title of the note:

  assert_selector 'form' do
    assert_selector :field, name: 'title'
  end

Ah, we have a few things going on here:

  1. assert_selector can be used in different ways

  2. There is a field type of selector

  3. We can pass attributes to verify a field

The documentation for assert_selector states the following:

assert_selector(*args, &optional_filter_block) โ‡’ Object

Parameters:
kind (Symbol) โ€” Optional selector type (:css, :xpath, :field, etc.). Defaults to default_selector.
locator (String) โ€” The locator for the specified selector
options (Hash) โ€” a customizable set of options

The kind parameter defaults to something called default_selector, which is defined by Capybara::Selector. It turns out we can make our assertions cleaner by using :field as our selector:

:field - Select field elements (input [not of type submit, image, or hidden], textarea, select)
Locator: Matches against the id, test_id attribute, name, placeholder, or associated label text
Filters:
:name (String, Regexp) - Matches the name attribute
:placeholder (String, Regexp) - Matches the placeholder attribute
:type (String) - Matches the type attribute of the field or element type for 'textarea' and 'select'
:readonly (Boolean) - Match on the element being readonly
:with (String, Regexp) - Matches the current value of the field
:checked (Boolean) - Match checked fields?
:unchecked (Boolean) - Match unchecked fields?
:disabled (Boolean, :all) - Match disabled field? (Default: false)
:multiple (Boolean) - Match fields that accept multiple values
:valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
:validation_message (String, Regexp) - Matches the elements current validationMessage

We can also use :button to verify the submit button renders correctly:

:button - Find buttons ( input [of type submit, reset, image, button] or button elements )
Locator: Matches the id, test_id attribute, name, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
Filters:
:name (String, Regexp) - Matches the name attribute
:title (String) - Matches the title attribute
:value (String) - Matches the value of an input button
:type (String) - Matches the type attribute
:disabled (Boolean, :all) - Match disabled buttons (Default: false)

Using name to identify the field's we want to verify, we will have the following assertions:


# Verify the title field
assert_selector 'form' do
  assert_selector :field, name: 'title'
end

# Verify the submit button
assert_selector 'form' do
  assert_selector :button, name: 'commit'
end

However, we have an issue. Path helpers don't work with view components, so using notes_path or notes_urls wouldn't work if we passed it in with form_options. To prevent the ActionController::UrlGenerationError error, we then need to wrap our assertions with with_request_url. Let's keep it simple and hardcode the url it:

with_request_url '/notes/new' do
  render_inline(NoteFormComponent.new(form_options:))

  assert_selector 'form' do
    assert_selector :field, name: 'title'
  end
end

Custom Assertions

As a bonus to this article, we're going to write a helper we can use to assert that the trix editor is rendered within our view component. Let's create a capybara_helpers.rb file and place it in test/support. You might need to create the support directory as it's not a default with built-in Rails usage of Minitest.

# test/support/capybara_helpers.rb

def assert_trix_editor(id)
  assert_selector(:xpath, "//*[@id='#{id}']", visible: false)
end

Once you've got the helper written, we need to ensure that we load this file in, we can do that in test_helpers.rb:

# test/test_helper.rb

Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }

Place this before the module ActiveSupport:

ENV['RAILS_ENV'] ||= 'test'

require_relative '../config/environment'
require 'rails/test_help'

Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }

module ActiveSupport
  class TestCase
    # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
    fixtures :all
    # Add more helper methods to be used by all tests here...
  end
end

Now that we have our capybara helpers being loaded, we can utilize it similarly to our previous assertions:

with_request_url '/notes/new' do
  assert_selector 'form' do
    assert_trix_editor 'body'
  end
end

The final product of our test's will look something like this:

# test/components/note_form_component_test.rb

require 'test_helper'

class NoteFormComponentTest < ViewComponent::TestCase
  def form_options
    { form_options: { model: Note.new, url: '/notes', local: true } }
  end

  def test_component_renders_form_with_title_field
    with_request_url '/notes/new' do
      render_inline(NoteFormComponent.new(form_options:))

      assert_selector 'form' do
        assert_selector :field, name: 'title'
      end
    end
  end

  def test_component_renders_form_with_summary_field
    with_request_url '/notes/new' do
      render_inline(NoteFormComponent.new(form_options:))

      assert_selector 'form' do
        assert_selector :field, name: 'summary'
      end
    end
  end

  def test_component_renders_form_with_content_field
    with_request_url '/notes/new' do
      render_inline(NoteFormComponent.new(form_options:))

      assert_selector 'form' do
        assert_trix_editor 'body'
      end
    end
  end

  def test_component_renders_form_with_submit_button
    with_request_url '/notes/new' do
      render_inline(NoteFormComponent.new(form_options:))

      assert_selector 'form' do
        assert_selector :button, name: 'commit'
      end
    end
  end
end

Conclusion

ViewComponent + Capybara is a perfect match for declaratively writing tests that verify your components state. This example is straightforward in that it only tests the default state of the form, but it demonstrates how simple it to write integration tests. These tests can be extended to use fill_in and click_button to ensure functionality. Why don't you give it a try? ๐Ÿ™‚


References

Testing the Trix Editor with Capybara and MiniTest

Capybara::Node::Matchers#assert_selector

Capybara::Selector

ViewComponent - Testing

0
Subscribe to my newsletter

Read articles from Alvin Crespo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alvin Crespo
Alvin Crespo

Hi, Iโ€™m Alvin! I write about tech, and love sharing what I learn. If you have any questions or suggestions, please reach out - I'm always up for a quick chat.