Rails Frontend Testing with JavaScript Insights
One of the newer things that Rails 5+ provides is system tests. This gives us a full frontend testing experience with Capybara, which runs each of our tests through a real browser window to perfectly provide what a normal web user would experience. Having this baked in to Rails makes for an elegant way to get started and hit the ground running.
What’s more, Rails will generate our standard CRUD system tests per resource when we use the Rails generator to generate it with the models name and attribute fields. This makes swapping in frontend frameworks a much more seamless experience.
In this post, we’ll tackle what changes to the code are required to get our previous Rails and VueJS posts app system tests all passing. And we’ll look at some JavaScript insights that might just save your day.
Updating a Frontend Application
The changes we’ll be demonstrating here are from where we left off in the VueJS Components with CoffeeScript for Rails post. You can find the source code for where we left off at danielpclark/vue_example.
To run your system tests, execute the following command in your project directory:
rails test:system
We should have two errors showing up for our create and update resources. The error message reads Unable to find visible field "Body" that is not disabled
.
We get the error about only this field because it’s the first error that’s come across in the test. If we swap our test order around, we’ll find that all of our fields in the create and update tests fail in this way. This has to do with the way Capybara looks up fields for testing.
When you use Rails’ form methods to generate labels and input fields, the HTML element label
will have a for
field. This contains the name of whatever input’s id
object you’re targeting. When we wrote a new form by hand with our VueJS components template, we didn’t write these fields in. So you’ll need to change the form fields in file app/javascript/form-document.vue.erb
from:
<label>Subject</label> <input type="text" v-model="document.subject" /> <label>State</label> <select v-model="document.state"> <%= options_for_select(Document.states.keys, "concept") %> </select> <label>Body</label> <textarea v-model="document.body"></textarea>
to the following:
<label for="document_subject">Subject</label> <input id="document_subject" type="text" v-model="document.subject" /> <label for="document_state">State</label> <select id="document_state" v-model="document.state"> <%= options_for_select(Document.states.keys, "concept") %> </select> <label for="document_body">Body</label> <textarea id="document_body" v-model="document.body"></textarea>
Now when you ask Capybara to fill_in
the Subject
, it gets the target by the label’s content and for
value and then targets the id
used in that for
value. What’s nice about this is you may use this label targeting technique in ways other than targeting an input field that follows.
If you try to run rails test:system
again, you’ll notice the same error messages are coming up. This has to do with a configuration in Rails 5 where the test environment isn’t using protect_from_forgery
, so the CSRF token is not being provided. Our VueJS code is failing because it explicitly requires that meta attribute field to be available.
You can fix this in one of these ways:
- Change your VueJS code so that it can work without a CSRF token present (which I advise against).
- Change your
config/environments/test.rb
file to have the following:
# Forgery protection config.action_controller.allow_forgery_protection = true
The following line of code may be required in your ApplicationController
for some frontend form submission implementations to work. For our use case, we won’t need it now:
protect_from_forgery prepend: true
To discover why this issue occurred, you need to run
rails server
with theRAILS_ENV=test
set. You can then see the JavaScript errors occurring in the browser’s console when you visit thenew
andedit
resource for our Document resource.
Now when you run rails test:system
, the test error message has changed, saying the buttons Update Document and Create Document cannot be found. That’s the naming scheme Rails would have generated for the submission button had we used their form helpers, so we’ll need to update the tests to reflect our current button’s name.
Open up test/system/documents_test.rb
and change the click_on
target from "Create Document"
and "Update Document"
to "Submit"
. Now running the tests, we get a new message saying the proper flash notification wasn’t found in the returned result. We’ll need to add in the flash messages by adding the following to our application template app/views/layouts/application.html.erb
just inside the <body>
tag:
<% flash.each do |name, msg| -%> <%= content_tag :div, msg, class: name %> <% end -%>
And then updating our controller’s update and new actions to have the appropriate flash notice in app/controllers/documents_controller.rb
:
def create @document = Document.new(document_params) respond_to do |format| if @document.save flash[:notice] = 'Document was successfully created.' format.html { redirect_to @document, notice: 'Document was successfully created.' } format.json { render :show, status: :created, location: @document } else format.html { render :new } format.json { render json: @document.errors, status: :unprocessable_entity } end end end def update respond_to do |format| if @document.update(document_params) flash[:notice] = 'Document was successfully updated.' format.html { redirect_to @document, notice: 'Document was successfully updated.' } format.json { render :show, status: :ok, location: @document } else format.html { render :edit } format.json { render json: @document.errors, status: :unprocessable_entity } end end end
And at this point, running rails test:system
, all our system tests will pass.
JavaScript Insights for Rails
Many Rails developers may not have a good grasp on JavaScript and its behavior when things go wrong. And in Rails, there are additional things that change its behavior in a peculiar way.
You may have heard many people express their dislike for Turbolinks and/or Spring. I happen to like both of these technologies, but if you don’t know what they do for you, they may lead even the most experienced JavaScript developers scratching their heads.
Let’s have a brief description of these two libraries.
Turbolinks is a tool that loads pages more quickly and provides a few quick-load UJS features for forms and some CRUD actions. The introduction of Turbolinks does change the JavaScript page ready hook you would normally use to "turbolinks:load"
. Using the standard JavaScript page ready hook causes issues when Turbolinks is in use.
Spring is a preloader for code in your application. It will load both Ruby code and frontend assets in its own process and cache what’s loaded to be available by the next request. If you don’t know how JavaScript handles errors, this can lead you to try to track down a bug for many days, as Spring will have things work after the first navigation from the first page load. So when the site loads, some things may seem not to work, but then you click a link and it starts working.
Let’s take a look first at how JavaScript handles bad code before delving in to Spring’s role in it.
When JavaScript comes across some bad code, or anything that evaluates to something it shouldn’t, JavaScript stops what it’s doing and produces some output in your browser’s console indicating that something is wrong. So when you introduce a new JavaScript library to your application and it has some bad code, then anything that follows it in your code, whether it’s more libraries or your own code, won’t run. This is the normal JavaScript behavior, to stop code execution at a point of bad code evaluation.
But when you add in Spring, which will load your assets in to a cache (with Turbolinks), Spring doesn’t avoid JavaScript code after bad JavaScript code evaluation. So you load the first page and Spring/Turbolinks caches your assets and libraries for faster load.
By the time you navigate through the first link, you may not see that anything is wrong because Spring produces the good JavaScript code after the bad. If a JavaScript feature isn’t needed on the first page load, then unless you’re looking for a problem in the console, you may not know anything is wrong. And when you go to publish your work, all of a sudden the site is partially down and not behaving as you expected even though “it works on my machine.”
This kind of thing has likely led many people to pull their hair out over not getting the same results on their machine as in production.
A good tip for JavaScript developers is that any time you add a new library or new JavaScript code to your application, put a simple console.log("Seems okay ¯\_(ツ)_/¯");
right after it. Then in your browser console, make sure that the code gets that far. Once you see the console log the output, you know that your JavaScript code didn’t come across any bad evaluation of code when loading.
I had tried a version of the Bootstrap 4 library, which has some bad JavaScript code evaluation in it, and I had placed it before the other JavaScript libraries were required in one of my applications. This caused all of the JavaScript related code not to work on the first page load but then appear to work on refresh and after the first link navigation. Because I had put it before Turbolinks that code was also behaving in this manner. To get it to work with Spring on the first page load, I put together this little hack in CoffeeScript:
if !Turbolinks? location.reload Turbolinks.dispatch("turbolinks:load")
This of course makes it work on my machine but won’t work in production. Developers in Rails often run into situations where the "turbolinks:load"
trigger isn’t called the first time the website’s first page loads.
The Turbolinks.dispatch("turbolinks:load")
is the way to instantly trigger that web hook as soon as that line of JavaScript is evaluated. This will come in handy for anyone who needs to get this to occur on first page load of their JavaScript/CoffeeScript scripts.
Triggering CoffeeScript on page load normally requires a little extra work since all CoffeeScript code is wrapped in a function and its scope is protected. Using the Turbolinks.dispatch("turoblinks:load")
method makes this not so complicated.
if !Turbolinks?; location.reload
is only a useful hack for when Spring is in use. It’s only needed to load JavaScript code in your development environment despite some bad JavaScript code evaluation occurring. Instead of using it, just use the console.log
technique to see how far your JavaScript code has successfully executed. Then remedy the bad code evaluation situation.
Summary
Capybara is a nice library for simple-to-write tests that work with your full frontend experience. Rails generates most of the proper Capybara tests for you so you can read the tests that are there and continue writing similar tests with what you learned from there. Rails does the heavy lifting of configuration and defaults so it’s very easy to get started with frontend testing.
Another additional nicety in Rails 5 is that whenever there is an exception raised in a system test, it captures a screenshot of the browser window’s contents and saves it to tmp/screenshots/
. Now you can see what’s on the screen and compare it to your test to better determine what’s wrong with it.
With an understanding of how JavaScript behaves with Rails’ libraries, we can prevent lots of debugging time we would otherwise get stuck with. This further increases our ability to use these powerful tools that have jaded others. Turbolinks and Spring make for a wonderful experience once you understand their effects and how to take advantage of them.
It feels good to be a Rails developer these days. Enjoy frontend testing!
Published on Web Code Geeks with permission by Daniel P. Clark, partner at our WCG program. See the original article here: Rails Frontend Testing with JavaScript Insights Opinions expressed by Web Code Geeks contributors are their own. |