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: "user@example.com"
form.fill_in :password, with: "secr3t"
form.check :remember_me
form.click_button
end
assert_text "Welcome back, user@example.com."
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: "updated.user@example.com"
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 valuesWhen submitting a
<form>through a call toform.click_button, you can pass anactionas the translation scope. Awith_form(scope:)call will default to thesubmitkey when one is not specified. For instance:with_form(scope: :post) do |form| form.click_button end
This call will search for an
<input type="text">or<button>whosevalueor text content is the String translated by thehelpers.submit.post.submitkey.That action can be overridden:
with_form(scope: :post) do |form| form.click_button :create end
-
model:- an instance of anActiveModel::ModelorActiveRecord::Baseto be used to translate Capybara's locator values, and to populate the fields with an attribute's value.For example, assuming that a
Postrecord has atitleattribute:post = Post.new(title: "The Title") with_form(model: post) do |form| form.fill_in :title end
The call to
form.fill_inwill search for an<input>element or a<textarea>element that is labelled by a<label>element whose value is translated from thehelpers.label.post.titleinternationalization key. If that element exists, set its value to the value ofpost.title(in this case,"The Title").An attribute's value can be overridden by providing a different value. For instance, assuming that a
Postrecord has atitleattribute:post = Post.create!(title: "Old Title") with_form(model: post) do |form| form.fill_in :title, with: "New Title" end
The call to
form.fill_inwill work much like the example above, with the exception that the providedwith:option's value (in this case,"New Title") will take precedence over thepost.titleattribute's value (in this case,"Old Title").When submitting a
<form>through a call toform.click_button, you can pass anactionas the translation scope. Awith_form(model:)call will determine the translation key based on themodelargument's persistence state.When a
modelinstance is a new record, the key will usecreate. For instance:post = Post.new with_form(model: post) do |form| form.click_button end
This call will search for an
<input type="text">or<button>whosevalueor text content is the String translated by thehelpers.submit.post.createkey.That action can be overridden:
post = Post.new with_form(model: post) do |form| form.click_button :submit end
When a
modelinstance is an existing persisted record, the key will useupdate. For instance:post = Post.last with_form(model: post) do |form| form.click_button end
This call will search for an
<input type="text">or<button>whosevalueor text content is the String translated by thehelpers.submit.post.updatekey.That action can be overridden:
post = Post.last with_form(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:
-
a singular
<input type="checkbox">element that corresponds to aBoolean-backedsupportedattribute, constructed byActionView::Helpers::FormBuilder#check_box -
a collection of
<input type="checkbox">elements that correspond to an association of relatedlanguagemodels, constructed byActionView::Helpers::FormBuilder#collection_check_boxes
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
endTo 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"
endwith_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:
-
a singular
<input type="checkbox">element that corresponds to aBoolean-backedsupportedattribute, constructed byActionView::Helpers::FormBuilder#check_box -
a collection of
<input type="checkbox">elements that correspond to an association of relatedLanguagemodels, constructed byActionView::Helpers::FormBuilder#collection_check_boxes
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
endTo 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"
endWhen 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
endThere 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, user@example.com."
end
endOr, 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
endInstallation
Add this line to your application's Gemfile:
gem 'with_form'And then execute:
$ bundleThen, include the WithForm::TestHelpers into your project testing framework.
MiniTest
# test/application_system_test_case.rb
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
include WithForm::TestHelpers
endRSpec
# spec/support/with_form.rb
RSpec.configure do |config|
config.include(WithForm::TestHelpers, type: :system)
endFAQ
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_formblock. 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?
- Formulaic's
fill_formandfill_form_and_submitare very useful abstractions oversimple_form-generated<form>elements. This gem's focus is at a different layer of abstraction. Instead of translating a Hash of attribute-value pairs into<form>element interactions, this gem's interface focussed on enhancing the experience of filling inform_with-generated<form>elements that are labelled by theActionView-provided internationalization tooling.
Contributing
See CONTRIBUTING.md.
License
The gem is available as open source under the terms of the MIT License.