WithForm

Your System Test's counterpart to form_with

Usage

Leverage Rails-generated <label> values to submit <form> elements in System Tests.

The with_form test helper

To add coverage to a form's fields that are generated by ActionView's form_with helper, fill them using with_form:

class UserInteractsWithFormsTest < ApplicationSystemTestCase
  include WithForm::TestHelpers

  test "user signs in" do
    visit new_session_path
    with_form(scope: :session) do |form|
      form.fill_in :email, with: "[email protected]"
      form.fill_in :password, with: "secr3t"
      form.check :remember_me
      form.click_button
    end

    assert_text "Welcome back, [email protected]."
  end

  test "user makes a post" do
    post = Post.new(title: "My First Post", tags: ["ruby", "testing"])

    visit new_post_path
    with_form(model: post) do |form|
      form.fill_in :title
      form.check :tags
      form.click_button
    end

    assert_text "Created Post: My First Post."
  end

  test "user updates their profile" do
    profile = Profile.create!

    visit profile_path
    with_form(model: profile) do |form|
      form.fill_in :email, with: "[email protected]"
      form.select "NY", from: :state
      form.click_button :update
    end

    assert_text "Your profile has been updated."
  end
end

with_form Options

The with_form helper method accepts two styles of options:

  • scope: - the internationalization scope key to use when translating Capybara's locator values

When submitting a <form> through a call to form.click_button, you can pass an action as the translation scope. A with_form(scope:) call will default to the submit key when one is not specified. For instance:

  form_with(scope: :post) do |form|
    form.click_button
  end

This call will search for an <input type="text"> or <button> whose value or text content is the String translated by the helpers.submit.post.submit key.

That action can be overridden:

  form_with(scope: :post) do |form|
    form.click_button :create
  end
  • model: - an instance of an ActiveModel::Model or ActiveRecord::Base to be used to translate Capybara's locator values, and to populate the fields with an attribute's value.

For example, assuming that a Post record has a title attribute:

  post = Post.new(title: "The Title")

  form_with(model: post) do |form|
    form.fill_in :title
  end

The call to form.fill_in will search for an <input> element or a <textarea> element that is labelled by a <label> element whose value is translated from the helpers.label.post.title internationalization key. If that element exists, set its value to the value of post.title (in this case, "The Title").

An attribute's value can be overridden by providing a different value. For instance, assuming that a Post record has a title attribute:

  post = Post.create!(title: "Old Title")

  form_with(model: post) do |form|
    form.fill_in :title, with: "New Title"
  end

The call to form.fill_in will work much like the example above, with the exception that the provided with: option's value (in this case, "New Title") will take precedence over the post.title attribute's value (in this case, "Old Title").

When submitting a <form> through a call to form.click_button, you can pass an action as the translation scope. A with_form(model:) call will determine the translation key based on the model argument's persistence state.

When a model instance is a new record, the key will use create. For instance:

  post = Post.new

  form_with(model: post) do |form|
    form.click_button
  end

This call will search for an <input type="text"> or <button> whose value or text content is the String translated by the helpers.submit.post.create key.

That action can be overridden:

  post = Post.new

  form_with(model: post) do |form|
    form.click_button :submit
  end

When a model instance is an existing persisted record, the key will use update. For instance:

  post = Post.last

  form_with(model: post) do |form|
    form.click_button
  end

This call will search for an <input type="text"> or <button> whose value or text content is the String translated by the helpers.submit.post.update key.

That action can be overridden:

  post = Post.last

  form_with(model: post) do |form|
    form.click_button :submit
  end

With the exception of #click_link and #click_link_or_button, the argument yielded to with_form supports all helper methods made available by Capybara::Node::Actions.

Those include:

  • attach_file(locator = nil, paths, make_visible: nil, **options)
  • check(locator, **options)
  • choose(locator, **options)
  • click_button(locator, nil, **options)
  • fill_in(locator, with: nil, **options)
  • select(value = nil, from: nil, **options)
  • uncheck(locator, **options)
  • unselect(value = nil, from: nil, **options)

check and uncheck support

The check and uncheck helpers can support a mixture of argument types and use cases.

with_form(scope:)

When a call to with_form is passed the scope: option, the check and uncheck helpers can accept both a String argument, or an Array argument populated with String values.

For example, consider the following features/new template:

<%# app/views/features/new.html.erb %>

<%= form_with(scope: :features) do |form| %>
  <%= form.label(:supported) %>
  <%= form.check_box(:supported) %>

  <%= form.label(:languages) %>
  <%= form.collection_check_boxes(
    :languages,
    [
      [ "Ruby", "ruby" ],
      [ "JavaScript", "js" ],
    ],
    :last,
    :first,
  ) %>
<% end %>

There are two styles of <input type="checkbox"> elements at-play in this template:

The corresponding check and uncheck method exposed by WithForm::TestHelpers can interact with both.

To check or checked the Boolean-backed <input type="checkbox"> elements, pass the attribute's name as a Symbol:

with_form scope: :features do |form| %>
  form.check :supported

  form.uncheck :supported
end

To check or checked the Array-backed <input type="checkbox"> elements, pass the values as either an Array of String values, or a singular String value:

with_form scope: :features do |form| %>
  form.check ["Ruby", "JavaScript"]

  form.uncheck "JavaScript"
end

with_form(model:)

When a call to with_form is passed the model: option, the check and uncheck helpers can accept a String argument, an Array argument populated with String values, or a singular Symbol argument.

For example, consider the following hypothetical models:

Next, consider rendering a <form> element within the features/new template:

<%# app/views/features/new.html.erb %>

<%= form_with(model: Feature.new) do |form| %>
  <%= form.label(:supported) %>
  <%= form.check_box(:supported) %>

  <%= form.label(:language_ids) %>
  <%= form.collection_check_boxes(
    :language_ids,
    Language.all,
    :id,
    :name,
  ) %>
<% end %>

There are two styles of <input type="checkbox"> elements at-play in this template:

The corresponding check and uncheck method exposed by WithForm::TestHelpers can interact with both.

To check or checked the Boolean-backed <input type="checkbox"> elements, pass the attribute's name as a Symbol:

with_form model: Feature.new(supported: false) do |form| %>
  form.check :supported

  form.uncheck :supported
end

To check or checked the Array-backed <input type="checkbox"> elements, pass the values as either an Array of String values, or a singular String value:

feature = Feature.new(languages: Language.all, supported: true)

with_form model: feature do |form| %>
  form.uncheck :supported

  form.uncheck feature.languages.map(&:name)

  form.check ["Ruby", "JavaScript"]

  form.uncheck "JavaScript"
end

When interacting with the Boolean-backed variation of the <input type="checkbox"> element through the form.check or form.uncheck calls, the end-state of the <input> element will always correspond to the variation of check or uncheck.

More directly stated: calls to check will always result in <input type="checkbox" checked>, and calls to uncheck will always result in <input type="checkbox">, regardless of the value of Feature#supported.

If your intention is that the <input> have the checked attribute, call check. If your intention is that the <input> not have the checked attribute, call uncheck.

ActionText rich_text_area support

When ActionText is available, with_form provides a #fill_in_rich_text_area helper method.

The current implementation is inspired by ActionText::SystemTestHelper#fill_in_rich_text_area that is currently declared on the current rails@master branch.

class UserInteractsWithRichTextAreasTest < ApplicationSystemTestCase
  include WithForm::TestHelpers

  test "makes a post with a scope: argument" do
    visit new_post_path
    with_form(scope: :post) do |form|
      form.fill_in_rich_text_area :body, with: "My First Post"
      form.click_button
    end

    assert_text "My First Post"
  end

  test "user makes a post with a model: argument" do
    post = Post.new(body: "My First Post")

    visit new_post_path
    with_form(model: post) do |form|
      form.fill_in_rich_text_area :body
      form.click_button
    end

    assert_text "My First Post"
  end
end

There is a current limitation in how the rails@master-inspired #fill_in_rich_text_area implementation resolves the locator argument. Since the underlying <trix-editor> element is not a default field provided by the browser, focussing on its corresponding <label> element won't focus the <trix-editor>. To resolve that shortcoming, the #fill_in_rich_text_area uses the <trix-editor aria-label="..."> attribute as the label text.

This is a helpful, but incomplete solution to the problem. This requires that instead of declaring a <label for="my_rich_text_field"> element referencing the <trix-editor id="my_rich_text_field"> element, the <label> element's text (or rather, the text that would be in the <label> element) must be passed to the <trix-editor aria-label="..."> attribute.

For example:

<%= form_with(model: Post.new) do |form %>
  <%= form.label :my_rich_text_field %>
  <%= form.rich_text_area :my_rich_text_field, "aria-label": translate(:my_rich_text_field, scope: "helpers.label.post") %>
<% end %>

The label and submit test helpers

While with_form can simplify <form> element interactions with multiple steps, there are times when a single line of instructions is more convenient.

Behind the scenes, with_form utilize the #label and #submit helper methods to translate <label> and <button> text, along with <input type="submit"> values.

To put the same helpers to use within your test, include the WithForm::TranslationHelpers module and invoke either:

  • label(model_name, attribute)

  • submit(model_name, action = :submit)

For example when clicking an <input type="checkbox"> labelled by a translation declared at helpers.label.session.ready:

class UserInteractsWithFormsTest < ApplicationSystemTestCase
  include WithForm::TranslationHelpers

  test "user ticks a box" do
    visit new_session_path
    check label(:session, :ready)

    assert_text "We're glad you're ready, [email protected]."
  end
end

Or, to destroy a Post by clicking a button labelled by a translation declared at helpers.submit.post.destroy:

class UserInteractsWithFormsTest < ApplicationSystemTestCase
  include WithForm::TranslationHelpers

  test "user deletes a post" do
    visit new_post_path
    click_on submit(:post, :destroy)

    assert_text "Deleted Post."
  end
end

Installation

Add this line to your application's Gemfile:

gem 'with_form'

And then execute:

$ bundle

Then, include the WithForm::TestHelpers into your project testing framework.

MiniTest

# test/application_system_test_case.rb
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  include WithForm::TestHelpers
end

RSpec

# spec/support/with_form.rb
RSpec.configure do |config|
  config.include(WithForm::TestHelpers, type: :system)
end

FAQ

I want to call a Capybara helper with that input's id attribute or name attribute. How can I do that?

  • You can mix the object that you invoke the helper methods on within the with_form block. For instance:
  with_form(scope: :post) do |form|
    form.fill_in :title, with: "The Post's Title"
    fill_in "another-field-id", with: "Another Value"
    fill_in "post[special-field]", with: "Special Value"
  end

I've used the formulaic gem before. How is this gem different?

Contributing

See CONTRIBUTING.md.

License

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